diff --git a/poetry/core/factory.py b/poetry/core/factory.py index d248dbb74..8a64a64e3 100644 --- a/poetry/core/factory.py +++ b/poetry/core/factory.py @@ -27,6 +27,11 @@ class Factory(object): Factory class to create various elements needed by Poetry. """ + DEPRECATED_CONSTRAINT_KEY_CURRENT_KEY_MAP = { + "allows_prereleases": "allow_prereleases", + "develop": "editable", + } + def create_poetry( self, cwd=None, with_dev=True ): # type: (Optional[Path]. bool) -> Poetry @@ -182,49 +187,40 @@ def create_dependency( constraint = "*" if isinstance(constraint, dict): - optional = constraint.get("optional", False) - python_versions = constraint.get("python") - platform = constraint.get("platform") - markers = constraint.get("markers") - if "allows-prereleases" in constraint: - message = ( - 'The "{}" dependency specifies ' - 'the "allows-prereleases" property, which is deprecated. ' - 'Use "allow-prereleases" instead.'.format(name) - ) - warn(message, DeprecationWarning) - logger.warning(message) - - allows_prereleases = constraint.get( - "allow-prereleases", constraint.get("allows-prereleases", False) + constraint_without_deprecated_keys = cls.as_constraint_with_deprecated_keys_renamed_to_current_keys( + dependency_name=name, constraint=constraint, ) + optional = constraint_without_deprecated_keys.get("optional", False) + python_versions = constraint_without_deprecated_keys.get("python") + platform = constraint_without_deprecated_keys.get("platform") + markers = constraint_without_deprecated_keys.get("markers") - if "git" in constraint: + if "git" in constraint_without_deprecated_keys: # VCS dependency dependency = VCSDependency( name, "git", - constraint["git"], - branch=constraint.get("branch", None), - tag=constraint.get("tag", None), - rev=constraint.get("rev", None), + constraint_without_deprecated_keys["git"], + branch=constraint_without_deprecated_keys.get("branch", None), + tag=constraint_without_deprecated_keys.get("tag", None), + rev=constraint_without_deprecated_keys.get("rev", None), category=category, optional=optional, - develop=constraint.get("develop", False), - extras=constraint.get("extras", []), + editable=constraint_without_deprecated_keys.get("editable", False), + extras=constraint_without_deprecated_keys.get("extras", []), ) - elif "file" in constraint: - file_path = Path(constraint["file"]) + elif "file" in constraint_without_deprecated_keys: + file_path = Path(constraint_without_deprecated_keys["file"]) dependency = FileDependency( name, file_path, category=category, base=root_dir, - extras=constraint.get("extras", []), + extras=constraint_without_deprecated_keys.get("extras", []), ) - elif "path" in constraint: - path = Path(constraint["path"]) + elif "path" in constraint_without_deprecated_keys: + path = Path(constraint_without_deprecated_keys["path"]) if root_dir: is_file = root_dir.joinpath(path).is_file() @@ -238,7 +234,7 @@ def create_dependency( category=category, optional=optional, base=root_dir, - extras=constraint.get("extras", []), + extras=constraint_without_deprecated_keys.get("extras", []), ) else: dependency = DirectoryDependency( @@ -247,27 +243,31 @@ def create_dependency( category=category, optional=optional, base=root_dir, - develop=constraint.get("develop", False), - extras=constraint.get("extras", []), + editable=constraint_without_deprecated_keys.get( + "editable", False + ), + extras=constraint_without_deprecated_keys.get("extras", []), ) - elif "url" in constraint: + elif "url" in constraint_without_deprecated_keys: dependency = URLDependency( name, - constraint["url"], + constraint_without_deprecated_keys["url"], category=category, optional=optional, - extras=constraint.get("extras", []), + extras=constraint_without_deprecated_keys.get("extras", []), ) else: - version = constraint["version"] + version = constraint_without_deprecated_keys["version"] dependency = Dependency( name, version, optional=optional, category=category, - allows_prereleases=allows_prereleases, - extras=constraint.get("extras", []), + allows_prereleases=constraint_without_deprecated_keys.get( + "allow_prereleases", False + ), + extras=constraint_without_deprecated_keys.get("extras", []), ) if not markers: @@ -296,7 +296,7 @@ def create_dependency( if not marker.is_any(): dependency.marker = marker - dependency.source_name = constraint.get("source") + dependency.source_name = constraint_without_deprecated_keys.get("source") else: dependency = Dependency(name, constraint, category=category) @@ -329,12 +329,23 @@ def validate( if not isinstance(constraint, dict): continue - if "allows-prereleases" in constraint: - result["warnings"].append( - 'The "{}" dependency specifies ' - 'the "allows-prereleases" property, which is deprecated. ' - 'Use "allow-prereleases" instead.'.format(name) - ) + for deprecated_key in cls.deprecated_keys(): + if deprecated_key in constraint: + if cls.constraint_has_deprecated_key_current_key_conflict( + constraint, deprecated_key + ): + result["errors"].append( + cls.deprecated_constraint_key_current_key_conflict_error_message( + dependency_name=name, + deprecated_key=deprecated_key, + ) + ) + else: + result["warnings"].append( + cls.constraint_key_deprecation_message( + dependency_name=name, key=deprecated_key + ) + ) # Checking for scripts with extras if "scripts" in config: @@ -371,3 +382,84 @@ def locate(cls, cwd): # type: (Path) -> Path cwd ) ) + + @classmethod + def deprecated_keys(cls): + for key in cls.DEPRECATED_CONSTRAINT_KEY_CURRENT_KEY_MAP: + yield key + + @classmethod + def as_constraint_with_deprecated_keys_renamed_to_current_keys( + cls, dependency_name, constraint + ): + constraint_with_renamed_keys = {} + for key, value in constraint.items(): + if cls.is_deprecated_constraint_key(key): + cls.raise_on_deprecated_constraint_key_current_key_conflict( + dependency_name, constraint, key + ) + cls.warn_constraint_key_is_deprecated(dependency_name, key) + current_key = cls.DEPRECATED_CONSTRAINT_KEY_CURRENT_KEY_MAP[key] + constraint_with_renamed_keys[current_key] = value + else: + constraint_with_renamed_keys[key] = value + return constraint_with_renamed_keys + + @classmethod + def raise_on_deprecated_constraint_key_current_key_conflict( + cls, dependency_name, constraint, deprecated_key + ): + """Raise `RuntimeError` when both a deprecated key and it's current, updated counterpart (key) are contained in constraint.""" + if cls.constraint_has_deprecated_key_current_key_conflict( + constraint, deprecated_key + ): + raise RuntimeError( + cls.deprecated_constraint_key_current_key_conflict_error_message( + dependency_name, deprecated_key, + ) + ) + + @classmethod + def constraint_has_deprecated_key_current_key_conflict( + cls, constraint, deprecated_key + ): + current_key = cls.DEPRECATED_CONSTRAINT_KEY_CURRENT_KEY_MAP[deprecated_key] + return current_key in constraint + + @classmethod + def deprecated_constraint_key_current_key_conflict_error_message( + cls, dependency_name, deprecated_key + ): + current_key = cls.DEPRECATED_CONSTRAINT_KEY_CURRENT_KEY_MAP[deprecated_key] + return ( + 'The "{dependency_name}" dependency specifies ' + 'both the "{current_key}" property and the deprecated "{deprecated_key}" property. ' + 'Please remove "{deprecated_key}" and resolve value conflicts!'.format( + dependency_name=dependency_name, + current_key=current_key, + deprecated_key=deprecated_key, + ) + ) + + @classmethod + def is_deprecated_constraint_key(cls, key): + return key in cls.DEPRECATED_CONSTRAINT_KEY_CURRENT_KEY_MAP + + @classmethod + def warn_constraint_key_is_deprecated(cls, dependency_name, key): + message = cls.constraint_key_deprecation_message(dependency_name, key) + warn(message, DeprecationWarning) + logging.warning(message) + + @classmethod + def constraint_key_deprecation_message(cls, dependency_name, key): + current_key = cls.DEPRECATED_CONSTRAINT_KEY_CURRENT_KEY_MAP[key] + return ( + 'The "{dependency_name}" dependency specifies ' + 'the "{deprecated_key}" property, which is deprecated. ' + 'Use "{current_key}" instead.'.format( + dependency_name=dependency_name, + deprecated_key=key, + current_key=current_key, + ) + ) diff --git a/poetry/core/json/schemas/poetry-schema.json b/poetry/core/json/schemas/poetry-schema.json index 81664910f..9acd830f5 100644 --- a/poetry/core/json/schemas/poetry-schema.json +++ b/poetry/core/json/schemas/poetry-schema.json @@ -359,9 +359,14 @@ "type": "string" } }, + "editable": { + "type": "boolean", + "description": "Whether to install the dependency in editable mode." + }, "develop": { "type": "boolean", - "description": "Whether to install the dependency in development mode." + "description": "Whether to install the dependency in editable mode.", + "deprecated": true } } }, @@ -435,9 +440,14 @@ "type": "string" } }, + "editable": { + "type": "boolean", + "description": "Whether to install the dependency in editable mode." + }, "develop": { "type": "boolean", - "description": "Whether to install the dependency in development mode." + "description": "Whether to install the dependency in editable mode.", + "deprecated": true } } }, diff --git a/poetry/core/packages/directory_dependency.py b/poetry/core/packages/directory_dependency.py index 0e7c920ee..e40e6b3b8 100644 --- a/poetry/core/packages/directory_dependency.py +++ b/poetry/core/packages/directory_dependency.py @@ -16,7 +16,7 @@ def __init__( category="main", # type: str optional=False, # type: bool base=None, # type: Path - develop=False, # type: bool + editable=False, # type: bool extras=None, # type: Union[List[str], Set[str]] ): self._path = path @@ -29,7 +29,7 @@ def __init__( except FileNotFoundError: raise ValueError("Directory {} does not exist".format(self._path)) - self._develop = develop + self._editable = editable self._supports_poetry = False if not self._full_path.exists(): @@ -75,8 +75,8 @@ def base(self): return self._base @property - def develop(self): - return self._develop + def editable(self): + return self._editable def supports_poetry(self): return self._supports_poetry @@ -91,7 +91,7 @@ def with_constraint(self, constraint): base=self.base, optional=self.is_optional(), category=self.category, - develop=self._develop, + editable=self._editable, extras=self._extras, ) diff --git a/poetry/core/packages/package.py b/poetry/core/packages/package.py index aac39afa7..0721826ca 100644 --- a/poetry/core/packages/package.py +++ b/poetry/core/packages/package.py @@ -96,7 +96,7 @@ def __init__( self.root_dir = None - self.develop = True + self.editable = True @property def name(self): @@ -319,7 +319,7 @@ def to_dependency(self): category=self.category, optional=self.optional, base=self.root_dir, - develop=self.develop, + editable=self.editable, extras=self.features, ) elif self.source_type == "file": @@ -348,7 +348,7 @@ def to_dependency(self): resolved_rev=self.source_resolved_reference, category=self.category, optional=self.optional, - develop=self.develop, + editable=self.editable, extras=self.features, ) else: @@ -405,7 +405,7 @@ def clone(self): # type: () -> "Package" clone.marker = self.marker clone.extras = self.extras clone.root_dir = self.root_dir - clone.develop = self.develop + clone.editable = self.editable for dep in self.requires: clone.requires.append(dep) diff --git a/poetry/core/packages/vcs_dependency.py b/poetry/core/packages/vcs_dependency.py index 6e4248668..42565df44 100644 --- a/poetry/core/packages/vcs_dependency.py +++ b/poetry/core/packages/vcs_dependency.py @@ -23,7 +23,7 @@ def __init__( resolved_rev=None, category="main", optional=False, - develop=False, + editable=False, extras=None, # type: Union[List[str], Set[str]] ): self._vcs = vcs @@ -36,7 +36,7 @@ def __init__( self._branch = branch self._tag = tag self._rev = rev - self._develop = develop + self._editable = editable super(VCSDependency, self).__init__( name, @@ -71,9 +71,16 @@ def tag(self): def rev(self): return self._rev + @property + def editable(self): # type: () -> bool + return self._editable + @property def develop(self): # type: () -> bool - return self._develop + raise DeprecationWarning( + "The `develop` key is deprecated, please use `editable` instead!" + ) + return self.editable @property def reference(self): # type: () -> str @@ -127,7 +134,7 @@ def with_constraint(self, constraint): resolved_rev=self._source_resolved_reference, optional=self.is_optional(), category=self.category, - develop=self._develop, + editable=self._editable, extras=self._extras, ) diff --git a/tests/fixtures/project_with_deprecated_develop_dependency/pyproject.toml b/tests/fixtures/project_with_deprecated_develop_dependency/pyproject.toml new file mode 100644 index 000000000..fbc3a9820 --- /dev/null +++ b/tests/fixtures/project_with_deprecated_develop_dependency/pyproject.toml @@ -0,0 +1,9 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "" +authors = ["Awesome Hacker "] + +[tool.poetry.dependencies] +python = "3.9.0" +pendulum = { git = "https://github.com/sdispater/pendulum.git", develop = false } diff --git a/tests/fixtures/project_with_editable_and_deprecated_develop_dependency/pyproject.toml b/tests/fixtures/project_with_editable_and_deprecated_develop_dependency/pyproject.toml new file mode 100644 index 000000000..f546cd9fe --- /dev/null +++ b/tests/fixtures/project_with_editable_and_deprecated_develop_dependency/pyproject.toml @@ -0,0 +1,9 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "" +authors = ["Awesome Hacker "] + +[tool.poetry.dependencies] +python = "3.9.0" +pendulum = { git = "https://github.com/sdispater/pendulum.git", editable = false, develop = false } diff --git a/tests/fixtures/project_with_invalid_dev_deps/pyproject.toml b/tests/fixtures/project_with_invalid_dev_deps/pyproject.toml index 1d0b5a846..0e5ff836d 100644 --- a/tests/fixtures/project_with_invalid_dev_deps/pyproject.toml +++ b/tests/fixtures/project_with_invalid_dev_deps/pyproject.toml @@ -10,4 +10,4 @@ license = "MIT" [tool.poetry.extras] [tool.poetry.dev-dependencies] -mylib = { path = "../mylib", develop = true} +mylib = { path = "../mylib", editable = false } diff --git a/tests/fixtures/sample_project/pyproject.toml b/tests/fixtures/sample_project/pyproject.toml index a26d33087..54651ae49 100644 --- a/tests/fixtures/sample_project/pyproject.toml +++ b/tests/fixtures/sample_project/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ python = "~2.7 || ^3.6" cleo = "^0.6" pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } -tomlkit = { git = "https://github.com/sdispater/tomlkit.git", rev = "3bff550", develop = false } +tomlkit = { git = "https://github.com/sdispater/tomlkit.git", rev = "3bff550", editable = false } requests = { version = "^2.18", optional = true, extras=[ "security" ] } pathlib2 = { version = "^2.2", python = "~2.7" } diff --git a/tests/masonry/builders/fixtures/with_bad_path_dep/pyproject.toml b/tests/masonry/builders/fixtures/with_bad_path_dep/pyproject.toml index 3c7edf13a..24a98b5f6 100644 --- a/tests/masonry/builders/fixtures/with_bad_path_dep/pyproject.toml +++ b/tests/masonry/builders/fixtures/with_bad_path_dep/pyproject.toml @@ -6,4 +6,4 @@ authors = ["Awesome Hacker "] [tool.poetry.dependencies] python = "^3.6" -bogus = { path = "../only/in/dev", develop = true } +bogus = { path = "../only/in/dev", editable = true } diff --git a/tests/masonry/builders/fixtures/with_bad_path_dev_dep/pyproject.toml b/tests/masonry/builders/fixtures/with_bad_path_dev_dep/pyproject.toml index 921d93af5..f0f82fc7b 100644 --- a/tests/masonry/builders/fixtures/with_bad_path_dev_dep/pyproject.toml +++ b/tests/masonry/builders/fixtures/with_bad_path_dev_dep/pyproject.toml @@ -8,4 +8,4 @@ authors = ["Awesome Hacker "] python = "^3.6" [tool.poetry.dev-dependencies] -bogus = { path = "../only/in/dev", develop = true } +bogus = { path = "../only/in/dev", editable = true } diff --git a/tests/test_factory.py b/tests/test_factory.py index 525fadb49..4eb5eb0df 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -2,6 +2,8 @@ from __future__ import absolute_import from __future__ import unicode_literals +import warnings + import pytest from poetry.core.factory import Factory @@ -49,7 +51,7 @@ def test_create_poetry(): assert pendulum.branch == "2.0" assert pendulum.source == "https://github.com/sdispater/pendulum.git" assert pendulum.allows_prereleases() - assert not pendulum.develop + assert not pendulum.editable tomlkit = dependencies["tomlkit"] assert tomlkit.pretty_constraint == "rev 3bff550" @@ -58,7 +60,7 @@ def test_create_poetry(): assert tomlkit.rev == "3bff550" assert tomlkit.source == "https://github.com/sdispater/tomlkit.git" assert tomlkit.allows_prereleases() - assert not tomlkit.develop + assert not tomlkit.editable requests = dependencies["requests"] assert requests.pretty_constraint == "^2.18" @@ -182,6 +184,37 @@ def test_validate_fails(): assert Factory.validate(content) == {"errors": [expected], "warnings": []} +def test_validate_strict_adds_error_when_dependency_contains_editable_and_deprecated_develop_key(): + config = TOMLFile( + fixtures_dir + / "project_with_editable_and_deprecated_develop_dependency/pyproject.toml" + ).read()["tool"]["poetry"] + expected = ( + 'The "pendulum" dependency specifies ' + 'both the "editable" property and the deprecated "develop" property. ' + 'Please remove "develop" and resolve value conflicts!' + ) + assert Factory.validate(config, strict=True) == { + "errors": [expected], + "warnings": [], + } + + +def test_validate_strict_adds_warning_when_dependency_contains_deprecated_develop_key(): + config = TOMLFile( + fixtures_dir / "project_with_deprecated_develop_dependency/pyproject.toml" + ).read()["tool"]["poetry"] + expected = ( + 'The "pendulum" dependency specifies ' + 'the "develop" property, which is deprecated. ' + 'Use "editable" instead.' + ) + assert Factory.validate(config, strict=True) == { + "errors": [], + "warnings": [expected], + } + + def test_create_poetry_fails_on_invalid_configuration(): with pytest.raises(RuntimeError) as e: Factory().create_poetry( @@ -217,3 +250,35 @@ def test_create_poetry_fails_with_invalid_dev_dependencies_iff_with_dev_is_true( Factory().create_poetry( fixtures_dir / "project_with_invalid_dev_deps", with_dev=False ) + + +def test_create_poetry_fails_when_dependency_contains_editable_and_deprecated_develop_key(): + with pytest.raises(RuntimeError) as e: + Factory().create_poetry( + fixtures_dir / "project_with_editable_and_deprecated_develop_dependency" + ) + assert ( + 'The "pendulum" dependency specifies ' + 'both the "editable" property and the deprecated "develop" property. ' + 'Please remove "develop" and resolve value conflicts!' + ) == str(e.value) + + +def test_create_poetry_warns_when_dependency_contains_deprecated_develop_key(caplog): + # All warnings are triggered inside this context manager. + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + expected = ( + 'The "pendulum" dependency specifies ' + 'the "develop" property, which is deprecated. ' + 'Use "editable" instead.' + ) + Factory().create_poetry( + fixtures_dir / "project_with_deprecated_develop_dependency" + ) + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert expected == str(w[-1].message) + for record in caplog.records: + assert record.levelname == "WARNING" + assert expected in caplog.text