-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for PEP621 with PDM (#155)
* feature: added a pdm dependency parser * Added support for PDM in dependency specification detector * added support for PDM in core.py * updated README and docs * added `DependencyManagementFormat` as `Enum` to replace string logic Co-authored-by: Mathieu Kniewallner <mathieu.kniewallner@gmail.com>
- Loading branch information
1 parent
26e55ac
commit bc461e9
Showing
15 changed files
with
500 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import logging | ||
import re | ||
from typing import Dict, List, Optional | ||
|
||
from deptry.dependency import Dependency | ||
from deptry.dependency_getter.base import DependenciesExtract, DependencyGetter | ||
from deptry.utils import load_pyproject_toml | ||
|
||
|
||
class PDMDependencyGetter(DependencyGetter): | ||
""" | ||
Class to get dependencies that are specified according to PEP 621 from a `pyproject.toml` file for a project that uses PDM for its dependency management. | ||
""" | ||
|
||
def get(self) -> DependenciesExtract: | ||
dependencies = self._get_pdm_dependencies() | ||
self._log_dependencies(dependencies) | ||
|
||
dev_dependencies = self._get_pdm_dev_dependencies() | ||
self._log_dependencies(dev_dependencies, is_dev=True) | ||
|
||
return DependenciesExtract(dependencies, dev_dependencies) | ||
|
||
@classmethod | ||
def _get_pdm_dependencies(cls) -> List[Dependency]: | ||
pyproject_data = load_pyproject_toml() | ||
dependency_strings: List[str] = pyproject_data["project"]["dependencies"] | ||
return cls._extract_dependency_objects_from(dependency_strings) | ||
|
||
@classmethod | ||
def _get_pdm_dev_dependencies(cls) -> List[Dependency]: | ||
""" | ||
Try to get development dependencies from pyproject.toml, which with PDM are specified as: | ||
[tool.pdm.dev-dependencies] | ||
test = [ | ||
"pytest", | ||
"pytest-cov", | ||
] | ||
tox = [ | ||
"tox", | ||
"tox-pdm>=0.5", | ||
] | ||
""" | ||
pyproject_data = load_pyproject_toml() | ||
|
||
dev_dependency_strings: List[str] = [] | ||
try: | ||
dev_dependencies_dict: Dict[str, str] = pyproject_data["tool"]["pdm"]["dev-dependencies"] | ||
for deps in dev_dependencies_dict.values(): | ||
dev_dependency_strings += deps | ||
except KeyError: | ||
logging.debug("No section [tool.pdm.dev-dependencies] found in pyproject.toml") | ||
|
||
return cls._extract_dependency_objects_from(dev_dependency_strings) | ||
|
||
@classmethod | ||
def _extract_dependency_objects_from(cls, pdm_dependencies: List[str]) -> List[Dependency]: | ||
dependencies = [] | ||
for spec in pdm_dependencies: | ||
# An example of a spec is `"tomli>=1.1.0; python_version < \"3.11\""` | ||
name = cls._find_dependency_name_in(spec) | ||
if name: | ||
optional = cls._is_optional(spec) | ||
conditional = cls._is_conditional(spec) | ||
dependencies.append(Dependency(name, conditional=conditional, optional=optional)) | ||
return dependencies | ||
|
||
@staticmethod | ||
def _is_optional(dependency_specification: str) -> bool: | ||
return bool(re.findall(r"\[([a-zA-Z0-9-]+?)\]", dependency_specification)) | ||
|
||
@staticmethod | ||
def _is_conditional(dependency_specification: str) -> bool: | ||
return ";" in dependency_specification | ||
|
||
@staticmethod | ||
def _find_dependency_name_in(spec: str) -> Optional[str]: | ||
match = re.search("[a-zA-Z0-9-_]+", spec) | ||
if match: | ||
return match.group(0) | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,70 +1,91 @@ | ||
import logging | ||
import os | ||
from enum import Enum | ||
from typing import Tuple | ||
|
||
from deptry.utils import load_pyproject_toml | ||
|
||
|
||
class DependencyManagementFormat(Enum): | ||
POETRY = "poetry" | ||
PDM = "pdm" | ||
REQUIREMENTS_TXT = "requirements_txt" | ||
|
||
|
||
class DependencySpecificationDetector: | ||
""" | ||
Class to detect how dependencies are specified: | ||
- Either find a pyproject.toml with a [poetry.tool.dependencies] section | ||
- Or find a requirements.txt. | ||
If both are found, pyproject.toml is preferred. | ||
- Otherwise, find a pyproject.toml with a [tool.pdm] section | ||
- Otherwise, find a requirements.txt. | ||
""" | ||
|
||
def __init__(self, requirements_txt: Tuple[str, ...] = ("requirements.txt",)) -> None: | ||
self.requirements_txt = requirements_txt | ||
|
||
def detect(self) -> str: | ||
uses_pyproject_toml = self._check_if_project_uses_pyproject_toml_for_dependencies() | ||
uses_requirements_txt = self._check_if_project_uses_requirements_txt_for_dependencies() | ||
if uses_pyproject_toml and uses_requirements_txt: | ||
logging.info( | ||
"Found both 'pyproject.toml' with a [tool.poetry.dependencies] section and" | ||
f" '{', '.join(self.requirements_txt)}' requirements file(s). Defaulting to 'pyproject.toml'.\n" | ||
) | ||
return "pyproject_toml" | ||
elif uses_pyproject_toml: | ||
def detect(self) -> DependencyManagementFormat: | ||
pyproject_toml_found = self._project_contains_pyproject_toml() | ||
if pyproject_toml_found and self._project_uses_poetry(): | ||
return DependencyManagementFormat.POETRY | ||
if pyproject_toml_found and self._project_uses_pdm(): | ||
return DependencyManagementFormat.PDM | ||
if self._project_uses_requirements_txt(): | ||
return DependencyManagementFormat.REQUIREMENTS_TXT | ||
raise FileNotFoundError( | ||
"No file called 'pyproject.toml' with a [tool.poetry.dependencies] or [tool.pdm] section or file(s)" | ||
f" called '{', '.join(self.requirements_txt)}' found. Exiting." | ||
) | ||
|
||
@staticmethod | ||
def _project_contains_pyproject_toml() -> bool: | ||
if "pyproject.toml" in os.listdir(): | ||
logging.debug("pyproject.toml found!") | ||
return True | ||
else: | ||
logging.debug("No pyproject.toml found.") | ||
return False | ||
|
||
@staticmethod | ||
def _project_uses_poetry() -> bool: | ||
pyproject_toml = load_pyproject_toml() | ||
try: | ||
pyproject_toml["tool"]["poetry"]["dependencies"] | ||
logging.debug( | ||
"Dependency specification found in 'pyproject.toml'. Will use this to determine the project's" | ||
" dependencies.\n" | ||
"pyproject.toml contains a [tool.poetry.dependencies] section, so Poetry is used to specify the" | ||
" project's dependencies." | ||
) | ||
return "pyproject_toml" | ||
elif uses_requirements_txt: | ||
return True | ||
except KeyError: | ||
logging.debug( | ||
f"Dependency specification found in '{', '.join(self.requirements_txt)}'. Will use this to determine" | ||
" the project's dependencies.\n" | ||
) | ||
return "requirements_txt" | ||
else: | ||
raise FileNotFoundError( | ||
"No file called 'pyproject.toml' with a [tool.poetry.dependencies] section or called" | ||
f" '{', '.join(self.requirements_txt)}' found. Exiting." | ||
"pyproject.toml does not contain a [tool.poetry.dependencies] section, so PDM is not used to specify" | ||
" the project's dependencies." | ||
) | ||
pass | ||
return False | ||
|
||
@staticmethod | ||
def _check_if_project_uses_pyproject_toml_for_dependencies() -> bool: | ||
if "pyproject.toml" in os.listdir(): | ||
logging.debug("pyproject.toml found!") | ||
pyproject_toml = load_pyproject_toml() | ||
try: | ||
pyproject_toml["tool"]["poetry"]["dependencies"] | ||
logging.debug( | ||
"pyproject.toml contains a [tool.poetry.dependencies] section, so it is used to specify the" | ||
" project's dependencies." | ||
) | ||
return True | ||
except KeyError: | ||
logging.debug( | ||
"pyproject.toml does not contain a [tool.poetry.dependencies] section, so it is not used to specify" | ||
" the project's dependencies." | ||
) | ||
pass | ||
|
||
def _project_uses_pdm() -> bool: | ||
pyproject_toml = load_pyproject_toml() | ||
try: | ||
pyproject_toml["tool"]["pdm"] | ||
logging.debug( | ||
"pyproject.toml contains a [tool.pdm] section, so PDM is used to specify the project's dependencies." | ||
) | ||
return True | ||
except KeyError: | ||
logging.debug( | ||
"pyproject.toml does not contain a [tool.pdm] section, so PDM is not used to specify" | ||
" the project's dependencies." | ||
) | ||
pass | ||
return False | ||
|
||
def _check_if_project_uses_requirements_txt_for_dependencies(self) -> bool: | ||
return any(os.path.isfile(requirements_txt) for requirements_txt in self.requirements_txt) | ||
def _project_uses_requirements_txt(self) -> bool: | ||
check = any(os.path.isfile(requirements_txt) for requirements_txt in self.requirements_txt) | ||
if check: | ||
logging.debug( | ||
f"Dependency specification found in '{', '.join(self.requirements_txt)}'. Will use this to determine" | ||
" the project's dependencies.\n" | ||
) | ||
return check |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import shlex | ||
import shutil | ||
import subprocess | ||
|
||
import pytest | ||
|
||
from deptry.utils import run_within_dir | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def pdm_dir_with_venv_installed(tmp_path_factory): | ||
tmp_path_proj = tmp_path_factory.getbasetemp() / "project_with_pdm" | ||
shutil.copytree("tests/data/project_with_pdm", str(tmp_path_proj)) | ||
with run_within_dir(tmp_path_proj): | ||
assert subprocess.check_call(shlex.split("pip install pdm; pdm install")) == 0 | ||
return tmp_path_proj | ||
|
||
|
||
def test_cli_with_pdm(pdm_dir_with_venv_installed): | ||
with run_within_dir(pdm_dir_with_venv_installed): | ||
result = subprocess.run(shlex.split("deptry ."), capture_output=True, text=True) | ||
assert result.returncode == 1 | ||
print(result.stderr) | ||
assert "The project contains obsolete dependencies:\n\n\tisort\n\trequests\n\n" in result.stderr | ||
assert "There are dependencies missing from the project's list of dependencies:\n\n\twhite\n\n" in result.stderr | ||
assert "There are imported modules from development dependencies detected:\n\n\tblack\n\n" in result.stderr |
Oops, something went wrong.