Як я можу визначити версію, визначену в setup.py (setuptools) у своєму пакеті?


Відповіді:


246

Допитувальний рядок версії вже встановленого дистрибутива

Щоб отримати версію зсередини вашого пакета під час виконання (про що, як видається, насправді задає ваше запитання), ви можете використовувати:

import pkg_resources  # part of setuptools
version = pkg_resources.require("MyProject")[0].version

Зберігати рядок версії для використання під час встановлення

Якщо ви хочете піти в інший бік (який, як видається, вважають, що інші автори відповідей тут думають, що ви запитуєте), покладіть рядок версії в окремий файл і прочитайте вміст цього файлу в setup.py.

Ви можете зробити version.py у своєму пакеті з __version__рядком, а потім прочитати його з setup.py, використовуючи execfile('mypackage/version.py'), щоб він встановився __version__у просторі імен setup.py.

Якщо ви хочете набагато простіший спосіб, який буде працювати з усіма версіями Python і навіть не-Python мовами, яким може знадобитися доступ до рядка версії:

Зберігайте рядок версії як єдиний вміст простого текстового файлу, названого наприклад VERSION, і читайте цей файл протягом setup.py.

version_file = open(os.path.join(mypackage_root_dir, 'VERSION'))
version = version_file.read().strip()

Тоді той самий VERSIONфайл буде працювати так само добре в будь-якій іншій програмі, навіть не-Python, і вам потрібно лише змінити рядок версій в одному місці для всіх програм.

Попередження про стан гонки під час встановлення

До речі, НЕ імпортуйте свій пакунок із свого setup.py, як це запропоновано в іншій відповіді тут: він, здається, спрацює для вас (адже у вас вже встановлені залежності вашого пакета), але він спричинить хаос для нових користувачів вашого пакету. , оскільки вони не зможуть встановити ваш пакет, не спершу вручну встановивши залежності.


1
Тут просто найкраща відповідь: найкращим способом є використання execfile, якщо вам потрібен setup.py, щоб отримати версію з вашого пакету.
тридцяте

2
execfileпрацює дуже добре ... але (на жаль) не працює з Python 3.
medmunds

9
... Гаразд, для використання Python 2 та 3 with open('mypackage/version.py') as f: exec(f.read())замість execfile('mypackage/version.py'). (З stackoverflow.com/a/437857/647002 )
medmunds

5
Частина руйнування хаосу не обов'язково відповідає дійсності ... вона спричинить хаос лише у тому випадку, якщо ваш файл init .py в пакеті має залежність від імпорту коду (прямо чи опосередковано).
Pykler

2
Здається, варто згадати, що django виконує __import__ дзвінок у їхньому setup.py . Чи безпечно це "__import__" vs "import"? Схоже, це не викликає у них проблем.
користувач1978019

33

Приклад дослідження: mymodule

Уявіть цю конфігурацію:

setup.py
mymodule/
        / __init__.py
        / version.py
        / myclasses.py

Тоді уявіть якийсь звичайний сценарій, коли у вас залежність і setup.pyвиглядає так:

setup(...
    install_requires=['dep1','dep2', ...]
    ...)

І приклад __init__.py:

from mymodule.myclasses import *
from mymodule.version import __version__

Наприклад myclasses.py:

# these are not installed on your system.
# importing mymodule.myclasses would give ImportError
import dep1
import dep2

проблема №1: імпорт mymoduleпід час налаштування

Якщо ваш setup.pyімпорт, mymoduleто під час налаштування ви, швидше за все, отримаєте ImportError. Це дуже поширена помилка, коли ваш пакет має залежності. Якщо ваш пакет не має інших залежностей від вбудованих, ви можете бути в безпеці; однак це не є хорошою практикою. Причиною тому є те, що це не є надійним для майбутнього; скажіть, завтра ваш код повинен споживати якусь іншу залежність.

проблема №2: де мій __version__?

Якщо ви жорстко __version__в setup.pyто воно може не збігатися з версією , що ви вантажили б у вашому модулі. Щоб бути послідовним, ви б помістили його в одне місце і прочитали з того самого місця, коли вам це потрібно. Використовуючи, importви можете отримати проблему №1.

рішення: à la setuptools

Ви могли б використовувати комбінацію open, execі забезпечити Dict для того execщоб додати змінні:

# setup.py
from setuptools import setup, find_packages
from distutils.util import convert_path

main_ns = {}
ver_path = convert_path('mymodule/version.py')
with open(ver_path) as ver_file:
    exec(ver_file.read(), main_ns)

setup(...,
    version=main_ns['__version__'],
    ...)

І в mymodule/version.pyвикритті версії:

__version__ = 'some.semantic.version'

Таким чином, версія постачається з модулем, і у вас не виникає проблем під час налаштування, намагаючись імпортувати модуль, у якого відсутні відсутні залежності (ще не встановлені).


2
Це здається найбільш розумним рішенням, оскільки воно покладається лише на дозволені залежності.
Dogweather

Чи відрізняється концепція версії пакета у файлі setup.py від версії ?
змінна

16

Найкращий спосіб - визначити __version__у своєму коді продукт, а потім імпортувати його в setup.py звідти. Це дає вам значення, яке ви можете прочитати у своєму запущеному модулі та маєте лише одне місце для його визначення.

Значення в setup.py не встановлюються, а setup.py не встановлюється після установки.

Що я робив (наприклад) у cover.py:

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

setup(
    name = 'coverage',
    version = __version__,
    ...
    )

ОНОВЛЕННЯ (2017): покриття.py більше не імпортує себе, щоб отримати версію. Імпорт власного коду може видалити його, оскільки код продукту намагатиметься імпортувати залежності, які ще не встановлені, тому що setup.py - це те, що їх встановлює.


4
@Evan: Я не впевнений, що ви отримуєте про те, що "лише витягуйте це значення з джерела". Якщо файл із __version__ним розміщений якимось чином, то імпорт також буде порушений. Python не знає, як лише інтерпретувати потрібні твердження. @pjeby має рацію: якщо вашому модулю потрібно імпортувати інші модулі, вони, можливо, ще не встановлені, і це буде хаотично. Ця методика працює, якщо ви обережні, що імпорт не заважає довгому ланцюгу іншого імпорту.
Нед Батчелдер

1
@Ned Batchelder Я впевнений, що, якщо ви поставите версію перед будь-яким імпортом у вихідний файл і лише до "з версії модуля імпорту", він не видасть більше вихідного файлу, ніж потрібно. Крім того, хто збирається випустити зламаний код? Якщо пакет потребує залежностей, використовуйте setuptools або чекайте випуску distutils2 пізніше року.
Еван Плейс

15
@Evan, вибачте, але ви неправильно імпортуєте часткові файли. Спробуйте помістити оператор друку в кінець довгого модуля та імпортуйте першу змінну, визначену у файл. Заява про друк буде виконана.
Нед Батчелдер

23
Я впав з цього приводу, але @pjeby цілком прав: "До речі, НЕ імпортуйте свій пакунок із свого setup.py, як запропоновано в іншій відповіді тут: це, здається, працює для вас (адже у вас вже встановлені залежності вашого пакета. ), але це спричинить загрозу для нових користувачів вашого пакету, оскільки вони не зможуть встановити ваш пакет, не спершу встановивши залежність вручну. " Нед, ти не проти додати попередження до своєї відповіді?
Доктор Ян-Філіп Герк

1
Як @ Jan-PhilipGehrcke , коли я завантажити файл setup.py , як це до PyPI , а потім pip install <packagename>, я отримую наступне повідомлення про помилку: ImportError: No module named <packagename>. Будь ласка, ЗАПЕРЕДУЙТЕ своїх читачів про те, що ви не можете запускати такі файли setup.py у середовищах, де пакет ще не встановлений!
варильні панелі

14

Ваше запитання трохи розпливчасте, але я думаю, що ви ставите питання, як його вказати.

Ви повинні визначитись __version__так:

__version__ = '1.4.4'

І тоді ви можете підтвердити, що setup.py знає про щойно вказану вами версію:

% ./setup.py --version
1.4.4

9

Я не був задоволений цими відповідями ... не хотів вимагати налаштувань, а також не робити цілого окремого модуля для однієї змінної, тому я придумав ці.

Бо коли ви впевнені, що основний модуль виконаний у стилі pep8 і залишиться таким:

version = '0.30.unknown'
with file('mypkg/mymod.py') as f:
    for line in f:
        if line.startswith('__version__'):
            _, _, version = line.replace("'", '').split()
            break

Якщо ви хочете бути дуже обережними та використовуйте справжній аналізатор:

import ast
version = '0.30.unknown2'
with file('mypkg/mymod.py') as f:
    for line in f:
        if line.startswith('__version__'):
            version = ast.parse(line).body[0].value.s
            break

setup.py - дещо викидний модуль, тому не проблема, якщо це трохи некрасиво.


Оновлення: досить смішно Я останнім часом відійшов від цього і почав використовувати окремий файл у пакеті під назвою meta.py. Я поміщаю туди багато метаданих, які, можливо, хочу часто змінювати. Отже, не лише на одне значення.


+1. Це порівняно просто, зберігає номер версії в одному місці, не потребує окремого файлу, щоб його містити, і не встановлює залежності імпорту модуля python від setup.py. Я б навіть не турбувався з контекстним менеджером у програмі настільки короткочасній, як setup.py.
ʇsәɹoɈ

Набагато приємніше, ніж використовувати регекс, дякую. Ви також можете скористатися ast.get_docstring()деякими .split('\n')[0].strip()тощо, щоб автоматично заповнити їх descriptionз джерела. Ще одна річ, яку потрібно синхронізувати
програв

4

Створіть файл у своєму вихідному дереві, наприклад, у yourbasedir / yourpackage / _version.py. Нехай цей файл містить лише один рядок коду, наприклад, такий:

__version__ = "1.1.0-r4704"

Потім у вашій setup.py відкрийте цей файл і проаналізуйте такий номер версії:

verstr = "невідомо"
спробуйте:
    verstrline = open ('yourpackage / _version.py', "rt"). read ()
крім EnvironmentError:
    pass # Гаразд, немає файлу версії.
ще:
    VSRE = r "^ __ версія__ = ['\"] ([^' \ "] *) ['\"] "
    mo = повторний пошук (VSRE, verstrline, re.M)
    якщо мо:
        verstr = mo.group (1)
    ще:
        підняти RuntimeError ("не вдається знайти версію у yourpackage / _version.py")

Нарешті, в yourbasedir/yourpackage/__init__.pyімпорті _версії, як це:

__version__ = "невідомо"
спробуйте:
    з імпорту _version __version__
крім ImportError:
    # Ми працюємо у дереві, яке не має _version.py, тому ми не знаємо, що таке наша версія.
    пропуск

Прикладом коду, який робить це, є пакет "pyutil", який я підтримую. (Див. PyPI або пошук у Google - stackoverflow забороняє мені включати гіперпосилання на нього у цю відповідь.)

@pjeby має рацію, що вам не слід імпортувати свій пакунок із власної setup.py. Це спрацює, коли ви протестуєте його, створивши новий інтерпретатор Python та виконайте в ньому setup.py перше:: python setup.pyале є випадки, коли він не працюватиме. Це тому, import youpackageщо не означає читати поточний робочий каталог для каталогу з назвою "yourpackage", це означає шукати в поточному sys.modulesключ "yourpackage", а потім робити різні речі, якщо його немає. Отже, це завжди працює, коли у python setup.pyвас є свіжий, порожній sys.modules, але це взагалі не працює.

Наприклад, що робити, якщо py2exe виконує ваш setup.py як частину процесу упаковки програми? Я бачив такий випадок, коли py2exe поставив би неправильний номер версії пакету, оскільки пакет отримував номер версії відimport myownthingу його setup.py, але інша версія цього пакета раніше була імпортована під час запуску py2exe. Крім того, що робити, якщо setuptools, easy_install, розповсюджувати або distutils2 намагаються створити ваш пакет як частину процесу встановлення іншого пакета, який залежить від вашого? Тоді чи важливо ваш пакет під час оцінювання його setup.py, чи вже існує версія вашого пакета, яка була імпортована протягом життя інтерпретатора Python, або чи потрібно для імпорту пакета спочатку встановити інші пакети. , або має побічні ефекти, може змінити результати. У мене було кілька проблем із спробою повторного використання пакетів Python, що спричинило проблеми для таких інструментів, як py2exe та setuptools, оскільки їх setup.py імпортує сам пакет, щоб знайти його номер версії.

До речі, ця методика чудово грає з інструментами для автоматичного створення yourpackage/_version.pyфайлу для вас, наприклад, зчитуючи історію контролю за редакцією та виписуючи номер версії на основі останнього тегу в історії контролю версій. Ось інструмент, який робить це для darcs: http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst, і ось фрагмент коду, який робить те ж саме для git: http: // github .com / warner / python-ecdsa / blob / 0ed702a9d4057ecf33eea969b8cf280eaccd89a1 / setup.py # L34


3

Це також має працювати, використовуючи регулярні вирази та залежно від полів метаданих мати такий формат:

__fieldname__ = 'value'

Використовуйте наступне на початку свого setup.py:

import re
main_py = open('yourmodule.py').read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", main_py))

Після цього ви можете використовувати метадані у своєму сценарії так:

print 'Author is:', metadata['author']
print 'Version is:', metadata['version']

Як я вже говорив вище, просто використовуйте str.split :)
Ерік Арауджо

О боже ні. Ви розбираєте Python в Python? Як мінімум, використовуйте дитину eval ().
шило

3
Погана порада про слабість, eval слід уникати, коли це легко.
Gringo Suave

3

З такою структурою:

setup.py
mymodule/
        / __init__.py
        / version.py
        / myclasses.py

де version.py містить:

__version__ = 'version_string'

Це можна зробити в setup.py :

import sys

sys.path[0:0] = ['mymodule']

from version import __version__

Це не спричинить жодних проблем із залежностями у вашому mymodule / __ init__.py


2

Щоб уникнути імпорту файлу (і, таким чином, виконувати його код), можна проаналізувати його та відновити versionатрибут із дерева синтаксису:

# assuming 'path' holds the path to the file

import ast

with open(path, 'rU') as file:
    t = compile(file.read(), path, 'exec', ast.PyCF_ONLY_AST)
    for node in (n for n in t.body if isinstance(n, ast.Assign)):
        if len(node.targets) == 1:
            name = node.targets[0]
            if isinstance(name, ast.Name) and \
                    name.id in ('__version__', '__version_info__', 'VERSION'):
                v = node.value
                if isinstance(v, ast.Str):
                    version = v.s
                    break
                if isinstance(v, ast.Tuple):
                    r = []
                    for e in v.elts:
                        if isinstance(e, ast.Str):
                            r.append(e.s)
                        elif isinstance(e, ast.Num):
                            r.append(str(e.n))
                    version = '.'.join(r)
                    break

Цей код намагається знайти __version__або VERSIONпризначення на верхньому рівні повернення модуля - це рядкове значення. Права сторона може бути або струною, або кортежем.


3
Це виглядає розумно і складно :) Було б простіше просто мати файл _version.py, що містить одне призначення та розбір, що з відкритим читанням-str.split.
Éric Araujo

Дякуємо, що опублікували це. Я думаю, що це занадто складно для цієї ситуації, але дуже корисно для розуміння того, як можна підійти до проблеми. Мені подобається, що він зберігає номер версії в одному місці, не вимагає, щоб окремий файл містив його, і не накладав залежності імпорту модуля python для сценарію настройки.
ʇsәɹoɈ

2

Є тисяча способів скинути кішку - ось моя:

# Copied from (and hacked):
# https://github.com/pypa/virtualenv/blob/develop/setup.py#L42
def get_version(filename):
    import os
    import re

    here = os.path.dirname(os.path.abspath(__file__))
    f = open(os.path.join(here, filename))
    version_file = f.read()
    f.close()
    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                              version_file, re.M)
    if version_match:
        return version_match.group(1)
    raise RuntimeError("Unable to find version string.")

2

Прибирання https://stackoverflow.com/a/12413800 від @ gringo-suave:

from itertools import ifilter
from os import path
from ast import parse

with open(path.join('package_name', '__init__.py')) as f:
    __version__ = parse(next(ifilter(lambda line: line.startswith('__version__'),
                                     f))).body[0].value.s

2

Зараз це є грубим і потребує певного вдосконалення (можливо, навіть є непокритий виклик учасника в pkg_resources, який я пропустив), але я просто не бачу, чому це не працює, і чому ніхто не запропонував цього на сьогодні (Googling around has не підключило це) ... зауважте, що це Python 2.x, і для нього знадобиться pkg_resources (зітхання):

import pkg_resources

version_string = None
try:
    if pkg_resources.working_set is not None:
        disto_obj = pkg_resources.working_set.by_key.get('<my pkg name>', None)
        # (I like adding ", None" to gets)
        if disto_obj is not None:
            version_string = disto_obj.version
except Exception:
    # Do something
    pass

2

Ми хотіли поставити мету інформації про наш пакеті pypackageryв__init__.py , але не міг , так як він має залежності сторонніх , як PJ Ебі вже зазначали (див його відповідь і попередження щодо стану гонки).

Ми вирішили це, створивши окремий модуль, pypackagery_meta.pyякий містить лише метаінформацію:

"""Define meta information about pypackagery package."""

__title__ = 'pypackagery'
__description__ = ('Package a subset of a monorepo and '
                   'determine the dependent packages.')
__url__ = 'https://github.com/Parquery/pypackagery'
__version__ = '1.0.0'
__author__ = 'Marko Ristin'
__author_email__ = 'marko.ristin@gmail.com'
__license__ = 'MIT'
__copyright__ = 'Copyright 2018 Parquery AG'

потім імпортувала метаінформацію у packagery/__init__.py:

# ...

from pypackagery_meta import __title__, __description__, __url__, \
    __version__, __author__, __author_email__, \
    __license__, __copyright__

# ...

і, нарешті, використали його setup.py:

import pypackagery_meta

setup(
    name=pypackagery_meta.__title__,
    version=pypackagery_meta.__version__,
    description=pypackagery_meta.__description__,
    long_description=long_description,
    url=pypackagery_meta.__url__,
    author=pypackagery_meta.__author__,
    author_email=pypackagery_meta.__author_email__,
    # ...
    py_modules=['packagery', 'pypackagery_meta'],
 )

Ви повинні включити pypackagery_metaу свій пакет py_modulesаргумент налаштування. В іншому випадку ви не можете імпортувати його після встановлення, оскільки пакетованому дистрибутиву цього не вистачає.


1

Простий і прямий, створіть файл, який викликається source/package_name/version.pyіз таким вмістом:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
__version__ = "2.6.9"

Потім у ваш файл source/package_name/__init__.pyви імпортуєте версію для використання іншими людьми:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from .version import __version__

Тепер ви можете покласти це setup.py

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys

try:
    filepath = 'source/package_name/version.py'
    version_file = open( filepath )
    __version__ ,= re.findall( '__version__ = "(.*)"', version_file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

finally:
    version_file.close()

Випробувано це з Python 2.7, 3.3, 3.4, 3.5, 3.6і 3.7на Linux, Windows і Mac OS. Я використовував у своєму пакеті, який має інтеграційні та одиничні тести для всіх тезних платформ. Ви можете побачити результати з .travis.ymlта appveyor.ymlтут:

  1. https://travis-ci.org/evandrocoan/debugtools/builds/527110800
  2. https://ci.appveyor.com/project/evandrocoan/pythondebugtools/builds/24245446

Альтернативна версія використовує менеджер контексту:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys

try:
    filepath = 'source/package_name/version.py'

    with open( filepath ) as file:
        __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

Ви також можете використовувати codecsмодуль для обробки помилок Unicode як на Python, так 2.7і на3.6

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
import codecs

try:
    filepath = 'source/package_name/version.py'

    with codecs.open( filepath, 'r', errors='ignore' ) as file:
        __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

Якщо ви пишете модуль Python на 100% у C / C ++ за допомогою розширень Python C, ви можете зробити те саме, але використовуючи C / C ++ замість Python.

У цьому випадку створіть наступне setup.py:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
import codecs
from setuptools import setup, Extension

try:
    filepath = 'source/version.h'

    with codecs.open( filepath, 'r', errors='ignore' ) as file:
        __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

setup(
        name = 'package_name',
        version = __version__,

        package_data = {
                '': [ '**.txt', '**.md', '**.py', '**.h', '**.hpp', '**.c', '**.cpp' ],
            },

        ext_modules = [
            Extension(
                name = 'package_name',
                sources = [
                    'source/file.cpp',
                ],
                include_dirs = ['source'],
            )
        ],
    )

Що читає версію з файлу version.h:

const char* __version__ = "1.0.12";

Але не забудьте створити файл MANIFEST.inдля включення version.h:

include README.md
include LICENSE.txt

recursive-include source *.h

І вона інтегрована в основну програму за допомогою:

#include <Python.h>
#include "version.h"

// create the module
PyMODINIT_FUNC PyInit_package_name(void)
{
    PyObject* thismodule;
    ...

    // https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue
    PyObject_SetAttrString( thismodule, "__version__", Py_BuildValue( "s", __version__ ) );

    ...
}

Список літератури:

  1. помилка відкритого файлу python
  2. Визначте глобальний модуль Python з API API
  3. Як включити дані пакета з setuptools / distribute?
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. Як використовувати пакети setuptools та ext_modules з однойменною назвою?
  6. Чи можна включати підкаталоги, використовуючи dist utils (setup.py) як частину даних пакета?

1

розгортати пакет на сервері та назви файлів для іменних пакетів:

приклад перетворення динамічної версії в pip:

  • виграти:

    • test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
    • test_pkg-1.0.0-py3.6-win-amd64.egg
  • Мак:

    • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.egg
    • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.whl
  • linux:
    • test_pkg-1.0.0-cp36-cp36m-linux_x86_64.whl
from setuptools_scm import get_version

def _get_version():

     dev_version = str(".".join(map(str, str(get_version()).split("+")[0]\
            .split('.')[:-1])))

    return dev_version

Знайдіть зразок setup.py, який викликає динамічну версію pip, що відповідає git commit

setup(
    version=_get_version(),
    name=NAME,
    description=DESCRIPTION,
    long_description=LONG_DESCRIPTION,
    classifiers=CLASSIFIERS,

# add few more for wheel wheel package ...conversion

)

0

Я використовую змінну середовища, як показано нижче

VERSION = 0.0.0 python setup.py sdist bdist_wheel

В setup.py


import os

setup(
    version=os.environ['VERSION'],
    ...
)

Для перевірки узгодженості з версією пакера я використовую нижче сценарій.

PKG_VERSION=`python -c "import pkg; print(pkg.__version__)"`
if [ $PKG_VERSION == $VERSION ]; then
    python setup.py sdist bdist_wheel
else
    echo "Package version differs from set env variable"
fi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.