Commit 98c077b9 by Sébastien Eustace

Improve classifiers management

parent a22683a8
...@@ -7,12 +7,14 @@ ...@@ -7,12 +7,14 @@
- Added compatibility with Python 3.4 and 3.5. - Added compatibility with Python 3.4 and 3.5.
- Added the `version` command to automatically bump the package's version. - Added the `version` command to automatically bump the package's version.
- Added a standalone installer to install `poetry` isolated. - Added a standalone installer to install `poetry` isolated.
- Added support for classifiers in `pyproject.toml`.
### Changed ### Changed
- Improved dependency resolution to avoid unnecessary operations. - Improved dependency resolution to avoid unnecessary operations.
- Improved dependency resolution speed. - Improved dependency resolution speed.
- Improved CLI reactivity by deferring imports. - Improved CLI reactivity by deferring imports.
- License classifer is not automatically added to classifers.
### Fixed ### Fixed
......
...@@ -10,6 +10,6 @@ class CheckCommand(Command): ...@@ -10,6 +10,6 @@ class CheckCommand(Command):
def handle(self): def handle(self):
# Load poetry and display errors, if any # Load poetry and display errors, if any
_ = self.poetry self.poetry.check(self.poetry.config, strict=True)
self.info('All set!') self.info('All set!')
...@@ -50,6 +50,10 @@ ...@@ -50,6 +50,10 @@
"type": "string", "type": "string",
"description": "The path to the README file" "description": "The path to the README file"
}, },
"classifiers": {
"type": "array",
"description": "A list of trove classifers."
},
"dependencies": { "dependencies": {
"type": "object", "type": "object",
"description": "This is a hash of package name (keys) and version constraints (values) that are required to run this package.", "description": "This is a hash of package name (keys) and version constraints (values) that are required to run this package.",
......
...@@ -52,8 +52,8 @@ class Metadata: ...@@ -52,8 +52,8 @@ class Metadata:
meta.home_page = package.homepage or package.repository_url meta.home_page = package.homepage or package.repository_url
meta.author = package.author_name meta.author = package.author_name
meta.author_email = package.author_email meta.author_email = package.author_email
meta.license = package.license meta.license = package.license.id
meta.classifiers = package.classifiers meta.classifiers = package.all_classifiers
# Version 1.2 # Version 1.2
meta.maintainer = meta.author meta.maintainer = meta.author
......
...@@ -5,6 +5,8 @@ from poetry.semver.constraints import Constraint ...@@ -5,6 +5,8 @@ from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint from poetry.semver.constraints import EmptyConstraint
from poetry.semver.helpers import parse_stability from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser from poetry.semver.version_parser import VersionParser
from poetry.spdx import license_by_id
from poetry.spdx import License
from poetry.version import parse as parse_version from poetry.version import parse as parse_version
from .constraints.generic_constraint import GenericConstraint from .constraints.generic_constraint import GenericConstraint
...@@ -68,7 +70,7 @@ class Package: ...@@ -68,7 +70,7 @@ class Package:
self.homepage = None self.homepage = None
self.repository_url = None self.repository_url = None
self.keywords = [] self.keywords = []
self.license = None self._license = None
self.readme = None self.readme = None
self.source_type = '' self.source_type = ''
...@@ -92,6 +94,8 @@ class Package: ...@@ -92,6 +94,8 @@ class Package:
self.include = [] self.include = []
self.exclude = [] self.exclude = []
self.classifiers = []
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = self._parser.parse_constraints('*')
self._platform = '*' self._platform = '*'
...@@ -187,10 +191,23 @@ class Package: ...@@ -187,10 +191,23 @@ class Package:
@property @property
def platform_constraint(self): def platform_constraint(self):
return self._platform_constraint return self._platform_constraint
@property
def license(self):
return self._license
@license.setter
def license(self, value):
if value is None:
self._license = license
elif isinstance(value, License):
self._license = license
else:
self._license = license_by_id(value)
@property @property
def classifiers(self): def all_classifiers(self):
classifiers = [] classifiers = self.classifiers.copy()
# Automatically set python classifiers # Automatically set python classifiers
parser = VersionParser() parser = VersionParser()
...@@ -210,7 +227,12 @@ class Package: ...@@ -210,7 +227,12 @@ class Package:
'Programming Language :: Python :: {}'.format(version) 'Programming Language :: Python :: {}'.format(version)
) )
return classifiers # Automatically set license classifers
classifiers.append(self.license.classifier)
classifiers = set(classifiers)
return sorted(classifiers)
def is_dev(self): def is_dev(self):
return self._dev return self._dev
......
...@@ -11,6 +11,7 @@ from .packages import Locker ...@@ -11,6 +11,7 @@ from .packages import Locker
from .packages import Package from .packages import Package
from .repositories import Pool from .repositories import Pool
from .repositories.pypi_repository import PyPiRepository from .repositories.pypi_repository import PyPiRepository
from .spdx import license_by_id
from .utils.toml_file import TomlFile from .utils.toml_file import TomlFile
...@@ -88,6 +89,7 @@ class Poetry: ...@@ -88,6 +89,7 @@ class Poetry:
package.repository_url = local_config.get('repository') package.repository_url = local_config.get('repository')
package.license = local_config.get('license') package.license = local_config.get('license')
package.keywords = local_config.get('keywords', []) package.keywords = local_config.get('keywords', [])
package.classifiers = local_config.get('classifiers', [])
if 'readme' in local_config: if 'readme' in local_config:
package.readme = Path(cwd) / local_config['readme'] package.readme = Path(cwd) / local_config['readme']
...@@ -163,4 +165,15 @@ class Poetry: ...@@ -163,4 +165,15 @@ class Poetry:
raise InvalidProjectFile(message) raise InvalidProjectFile(message)
if strict:
# If strict, check the file more thoroughly
# Checking license
license = config.get('license')
if license:
try:
license_by_id(license)
except ValueError:
raise InvalidProjectFile('Invalid license')
return True return True
import json
import os
from .license import License
from .updater import Updater
_licenses = None
def license_by_id(identifier):
if _licenses is None:
load_licenses()
id = identifier.lower()
if id not in _licenses:
raise ValueError('Invalid license id: {}'.format(identifier))
return _licenses[id]
def load_licenses():
global _licenses
_licenses = {}
licenses_file = os.path.join(
os.path.dirname(__file__),
'data',
'licenses.json'
)
with open(licenses_file) as f:
data = json.loads(f.read())
for name, license in data.items():
_licenses[name.lower()] = License(
name, license[0], license[1], license[2]
)
if __name__ == '__main__':
updater = Updater()
updater.dump()
from collections import namedtuple
class License(namedtuple('License', 'id name is_osi_approved is_deprecated')):
CLASSIFIER_SUPPORTED = {
# Not OSI Approved
'Aladdin',
'CC0-1.0',
'CECILL-B', 'CECILL-C',
'NPL-1.0', 'NPL-1.1',
# OSI Approved
'AFPL',
'AFL-1.1', 'AFL-1.2', 'AFL-2.0', 'AFL-2.1', 'AFL-3.0',
'Apache-1.1', 'Apache-2.0',
'APSL-1.1', 'APSL-1.2', 'APSL-2.0',
'Artistic-1.0', 'Artistic-2.0',
'AAL',
'AGPL-3.0', 'AGPL-3.0-only', 'AGPL-3.0-or-later',
'BSL-1.0',
'BSD-2-Clause', 'BSD-3-Clause',
'CDDL-1.0',
'CECILL-2.1',
'CPL-1.0',
'EFL-1.0', 'EFL-2.0',
'EPL-1.0', 'EPL-2.0',
'EUPL-1.1', 'EUPL-1.2',
'GPL-2.0', 'GPL-2.0+', 'GPL-2.0-only', 'GPL-2.0-or-later',
'GPL-3.0', 'GPL-3.0+', 'GPL-3.0-only', 'GPL-3.0-or-later',
'LGPL-2.0', 'LGPL-2.0+', 'LGPL-2.0-only', 'LGPL-2.0-or-later',
'LGPL-3.0', 'LGPL-3.0+', 'LGPL-3.0-only', 'LGPL-3.0-or-later',
'MIT',
'MPL-1.0', 'MPL-1.1', 'MPL-1.2',
'Nokia',
'W3C',
'ZPL-1.0', 'ZPL-2.0', 'ZPL-2.1',
}
CLASSIFIER_NAMES = {
# Not OSI Approved
'AFPL': 'Aladdin Free Public License (AFPL)',
'CC0-1.0': 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication',
'CECILL-B': 'CeCILL-B Free Software License Agreement (CECILL-B)',
'CECILL-C': 'CeCILL-C Free Software License Agreement (CECILL-C)',
'NPL-1.0': 'Netscape Public License (NPL)',
'NPL-1.1': 'Netscape Public License (NPL)',
# OSI Approved
'AFL-1.1': 'Academic Free License (AFL)',
'AFL-1.2': 'Academic Free License (AFL)',
'AFL-2.0': 'Academic Free License (AFL)',
'AFL-2.1': 'Academic Free License (AFL)',
'AFL-3.0': 'Academic Free License (AFL)',
'Apache-1.1': 'Apache Software License',
'Apache-2.0': 'Apache Software License',
'APSL-1.1': 'Apple Public Source License',
'APSL-1.2': 'Apple Public Source License',
'APSL-2.0': 'Apple Public Source License',
'Artistic-1.0': 'Artistic License',
'Artistic-2.0': 'Artistic License',
'AAL': 'Attribution Assurance License',
'AGPL-3.0': 'GNU Affero General Public License v3',
'AGPL-3.0-only': 'GNU Affero General Public License v3',
'AGPL-3.0-or-later': 'GNU Affero General Public License v3 or later (AGPLv3+)',
'BSL-1.0': 'Boost Software License 1.0 (BSL-1.0)',
'BSD-2-Clause': 'BSD License',
'BSD-3-Clause': 'BSD License',
'CDDL-1.0': 'Common Development and Distribution License 1.0 (CDDL-1.0)',
'CECILL-2.1': 'CEA CNRS Inria Logiciel Libre License, version 2.1 (CeCILL-2.1)',
'CPL-1.0': 'Common Public License',
'EPL-1.0': 'Eclipse Public License 1.0 (EPL-1.0)',
'EFL-1.0': 'Eiffel Forum License',
'EFL-2.0': 'Eiffel Forum License',
'EUPL-1.1': 'European Union Public Licence 1.1 (EUPL 1.1)',
'EUPL-1.2': 'European Union Public Licence 1.2 (EUPL 1.2)',
'GPL-2.0': 'GNU General Public License v2 (GPLv2)',
'GPL-2.0-only': 'GNU General Public License v2 (GPLv2)',
'GPL-2.0+': 'GNU General Public License v2 or later (GPLv2+)',
'GPL-2.0-or-later': 'GNU General Public License v2 or later (GPLv2+)',
'GPL-3.0': 'GNU General Public License v3 (GPLv3)',
'GPL-3.0-only': 'GNU General Public License v3 (GPLv3)',
'GPL-3.0+': 'GNU General Public License v3 or later (GPLv3+)',
'GPL-3.0-or-later': 'GNU General Public License v3 or later (GPLv3+)',
'LGPL-2.0': 'GNU Lesser General Public License v2 (LGPLv2)',
'LGPL-2.0-only': 'GNU Lesser General Public License v2 (LGPLv2)',
'LGPL-2.0+': 'GNU Lesser General Public License v2 or later (LGPLv2+)',
'LGPL-2.0-or-later': 'GNU Lesser General Public License v2 or later (LGPLv2+)',
'LGPL-3.0': 'GNU Lesser General Public License v3 (LGPLv3)',
'LGPL-3.0-only': 'GNU Lesser General Public License v3 (LGPLv3)',
'LGPL-3.0+': 'GNU Lesser General Public License v3 or later (LGPLv3+)',
'LGPL-3.0-or-later': 'GNU Lesser General Public License v3 or later (LGPLv3+)',
'MPL-1.0': 'Mozilla Public License 1.0 (MPL)',
'MPL-1.1': 'Mozilla Public License 1.1 (MPL 1.1)',
'MPL-2.0': 'Mozilla Public License 2.0 (MPL 2.0)',
'W3C': 'W3C License',
'ZPL-1.1': 'Zope Public License',
'ZPL-2.0': 'Zope Public License',
'ZPL-2.1': 'Zope Public License',
}
@property
def classifier(self):
parts = ['License']
if self.is_osi_approved:
parts.append('OSI Approved')
name = self.classifier_name
if name is not None:
parts.append(name)
return ' :: '.join(parts)
@property
def classifier_name(self):
if self.id not in self.CLASSIFIER_SUPPORTED:
if self.is_osi_approved:
return None
return 'Other/Proprietary License'
if self.id in self.CLASSIFIER_NAMES:
return self.CLASSIFIER_NAMES[self.id]
return self.name
import json
import os
from urllib.request import urlopen
class Updater:
BASE_URL = 'https://raw.githubusercontent.com/spdx/license-list-data/master/json/'
def __init__(self, base_url=BASE_URL):
self._base_url = base_url
def dump(self, file=None):
if file is None:
file = os.path.join(
os.path.dirname(__file__),
'data',
'licenses.json'
)
licenses_url = self._base_url + 'licenses.json'
with open(file, 'w') as f:
f.write(
json.dumps(
self.get_licenses(licenses_url),
indent=2,
sort_keys=True
)
)
def get_licenses(self, url):
licenses = {}
with urlopen(url) as r:
data = json.loads(r.read().decode())
for info in data['licenses']:
licenses[info['licenseId']] = [
info['name'],
info['isOsiApproved'],
info['isDeprecatedLicenseId']
]
return licenses
...@@ -15,6 +15,11 @@ documentation = "https://poetry.eustace.io/docs" ...@@ -15,6 +15,11 @@ documentation = "https://poetry.eustace.io/docs"
keywords = ["packaging", "dependency", "poetry"] keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements # Requirements
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.4" python = "^3.4"
......
...@@ -15,6 +15,10 @@ documentation = "https://poetry.eustace.io/docs" ...@@ -15,6 +15,10 @@ documentation = "https://poetry.eustace.io/docs"
keywords = ["packaging", "dependency", "poetry"] keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements # Requirements
[tool.poetry.dependencies] [tool.poetry.dependencies]
......
...@@ -15,6 +15,10 @@ documentation = "https://poetry.eustace.io/docs" ...@@ -15,6 +15,10 @@ documentation = "https://poetry.eustace.io/docs"
keywords = ["packaging", "dependency", "poetry"] keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements # Requirements
[tool.poetry.dependencies] [tool.poetry.dependencies]
......
...@@ -112,9 +112,12 @@ Keywords: packaging,dependency,poetry ...@@ -112,9 +112,12 @@ Keywords: packaging,dependency,poetry
Author: Sébastien Eustace Author: Sébastien Eustace
Author-email: sebastien@eustace.io Author-email: sebastien@eustace.io
Requires-Python: >= 3.6.0.0, < 4.0.0.0 Requires-Python: >= 3.6.0.0, < 4.0.0.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time Provides-Extra: time
Requires-Dist: cleo (>=0.6.0.0,<0.7.0.0) Requires-Dist: cleo (>=0.6.0.0,<0.7.0.0)
Requires-Dist: pendulum (>=1.4.0.0,<2.0.0.0); extra == "time" Requires-Dist: pendulum (>=1.4.0.0,<2.0.0.0); extra == "time"
......
from poetry.spdx import license_by_id
def test_classifier_name():
license = license_by_id('lgpl-3.0-or-later')
assert license.classifier_name == 'GNU Lesser General Public License v3 or later (LGPLv3+)'
def test_classifier_name_no_classifer_osi_approved():
license = license_by_id('LiLiQ-R-1.1')
assert license.classifier_name is None
def test_classifier_name_no_classifer():
license = license_by_id('Leptonica')
assert license.classifier_name == 'Other/Proprietary License'
def test_classifier():
license = license_by_id('lgpl-3.0-or-later')
assert license.classifier == (
'License :: '
'OSI Approved :: '
'GNU Lesser General Public License v3 or later (LGPLv3+)'
)
def test_classifier_no_classifer_osi_approved():
license = license_by_id('LiLiQ-R-1.1')
assert license.classifier == 'License :: OSI Approved'
def test_classifier_no_classifer():
license = license_by_id('Leptonica')
assert license.classifier == 'License :: Other/Proprietary License'
import pytest
from poetry.spdx import license_by_id
def test_license_by_id():
license = license_by_id('MIT')
assert license.id == 'MIT'
assert license.name == 'MIT License'
assert license.is_osi_approved
assert not license.is_deprecated
license = license_by_id('LGPL-3.0-or-later')
assert license.id == 'LGPL-3.0-or-later'
assert license.name == 'GNU Lesser General Public License v3.0 or later'
assert license.is_osi_approved
assert not license.is_deprecated
def test_license_by_id_is_case_insensitive():
license = license_by_id('mit')
assert license.id == 'MIT'
license = license_by_id('miT')
assert license.id == 'MIT'
def test_license_by_id_invalid():
with pytest.raises(ValueError):
license_by_id('invalid')
...@@ -17,7 +17,7 @@ def test_poetry(): ...@@ -17,7 +17,7 @@ def test_poetry():
assert package.version == '1.2.3' assert package.version == '1.2.3'
assert package.description == 'Some description.' assert package.description == 'Some description.'
assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>'] assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>']
assert package.license == 'MIT' assert package.license.id == 'MIT'
assert str(package.readme.relative_to(fixtures_dir)) == "sample_project/README.rst" assert str(package.readme.relative_to(fixtures_dir)) == "sample_project/README.rst"
assert package.homepage == 'https://poetry.eustace.io' assert package.homepage == 'https://poetry.eustace.io'
assert package.repository_url == 'https://github.com/sdispater/poetry' assert package.repository_url == 'https://github.com/sdispater/poetry'
...@@ -56,6 +56,24 @@ def test_poetry(): ...@@ -56,6 +56,24 @@ def test_poetry():
assert 'db' in package.extras assert 'db' in package.extras
classifiers = package.classifiers
assert classifiers == [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
assert package.all_classifiers == [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
def test_check(): def test_check():
complete = fixtures_dir / 'complete.toml' complete = fixtures_dir / 'complete.toml'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment