Skip to content

Commit

Permalink
Refactor code to support multiple project type
Browse files Browse the repository at this point in the history
  • Loading branch information
sdispater committed Apr 25, 2023
1 parent 097523a commit 9b3eb32
Show file tree
Hide file tree
Showing 16 changed files with 560 additions and 262 deletions.
174 changes: 14 additions & 160 deletions src/poetry/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,16 @@

from packaging.utils import canonicalize_name

from poetry.core.utils.helpers import combine_unicode
from poetry.core.pyproject.formats.content_format import ContentFormat
from poetry.core.utils.helpers import readme_content_type


if TYPE_CHECKING:
from collections.abc import Mapping

from poetry.core.packages.dependency import Dependency
from poetry.core.packages.dependency_group import DependencyGroup
from poetry.core.packages.project_package import ProjectPackage
from poetry.core.poetry import Poetry
from poetry.core.spdx.license import License

DependencyConstraint = Union[str, Dict[str, Any]]
DependencyConfig = Mapping[
Expand All @@ -46,180 +44,36 @@ def create_poetry(
from poetry.core.pyproject.toml import PyProjectTOML

poetry_file = self.locate(cwd)
local_config = PyProjectTOML(path=poetry_file).poetry_config
pyproject = PyProjectTOML(path=poetry_file)

if not pyproject.is_poetry_project():
raise RuntimeError(f"The project at {poetry_file} is not a Poetry project")

content_format = pyproject.content_format
assert isinstance(content_format, ContentFormat)

# Checking validity
check_result = self.validate(local_config)
if check_result["errors"]:
check_result = content_format.validate(strict=False)
if check_result.errors:
message = ""
for error in check_result["errors"]:
for error in check_result.errors:
message += f" - {error}\n"

raise RuntimeError("The Poetry configuration is invalid:\n" + message)

# Load package
name = local_config["name"]
assert isinstance(name, str)
version = local_config["version"]
assert isinstance(version, str)
package = self.get_package(name, version)
package = self.configure_package(
package, local_config, poetry_file.parent, with_groups=with_groups
package = content_format.to_package(
root=poetry_file.parent, with_groups=with_groups
)

return Poetry(poetry_file, local_config, package)
return Poetry(poetry_file, pyproject.poetry_config, package)

@classmethod
def get_package(cls, name: str, version: str) -> ProjectPackage:
from poetry.core.packages.project_package import ProjectPackage

return ProjectPackage(name, version)

@classmethod
def _add_package_group_dependencies(
cls,
package: ProjectPackage,
group: str | DependencyGroup,
dependencies: DependencyConfig,
) -> None:
from poetry.core.packages.dependency_group import MAIN_GROUP

if isinstance(group, str):
if package.has_dependency_group(group):
group = package.dependency_group(group)
else:
from poetry.core.packages.dependency_group import DependencyGroup

group = DependencyGroup(group)

for name, constraints in dependencies.items():
_constraints = (
constraints if isinstance(constraints, list) else [constraints]
)
for _constraint in _constraints:
if name.lower() == "python":
if group.name == MAIN_GROUP and isinstance(_constraint, str):
package.python_versions = _constraint
continue

group.add_dependency(
cls.create_dependency(
name,
_constraint,
groups=[group.name],
root_dir=package.root_dir,
)
)

package.add_dependency_group(group)

@classmethod
def configure_package(
cls,
package: ProjectPackage,
config: dict[str, Any],
root: Path,
with_groups: bool = True,
) -> ProjectPackage:
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.dependency_group import MAIN_GROUP
from poetry.core.packages.dependency_group import DependencyGroup
from poetry.core.spdx.helpers import license_by_id

package.root_dir = root

for author in config["authors"]:
package.authors.append(combine_unicode(author))

for maintainer in config.get("maintainers", []):
package.maintainers.append(combine_unicode(maintainer))

package.description = config.get("description", "")
package.homepage = config.get("homepage")
package.repository_url = config.get("repository")
package.documentation_url = config.get("documentation")
try:
license_: License | None = license_by_id(config.get("license", ""))
except ValueError:
license_ = None

package.license = license_
package.keywords = config.get("keywords", [])
package.classifiers = config.get("classifiers", [])

if "readme" in config:
if isinstance(config["readme"], str):
package.readmes = (root / config["readme"],)
else:
package.readmes = tuple(root / readme for readme in config["readme"])

if "dependencies" in config:
cls._add_package_group_dependencies(
package=package, group=MAIN_GROUP, dependencies=config["dependencies"]
)

if with_groups and "group" in config:
for group_name, group_config in config["group"].items():
group = DependencyGroup(
group_name, optional=group_config.get("optional", False)
)
cls._add_package_group_dependencies(
package=package,
group=group,
dependencies=group_config["dependencies"],
)

if with_groups and "dev-dependencies" in config:
cls._add_package_group_dependencies(
package=package, group="dev", dependencies=config["dev-dependencies"]
)

extras = config.get("extras", {})
for extra_name, requirements in extras.items():
extra_name = canonicalize_name(extra_name)
package.extras[extra_name] = []

# Checking for dependency
for req in requirements:
req = Dependency(req, "*")

for dep in package.requires:
if dep.name == req.name:
dep.in_extras.append(extra_name)
package.extras[extra_name].append(dep)

if "build" in config:
build = config["build"]
if not isinstance(build, dict):
build = {"script": build}
package.build_config = build or {}

if "include" in config:
package.include = []

for include in config["include"]:
if not isinstance(include, dict):
include = {"path": include}

formats = include.get("format", [])
if formats and not isinstance(formats, list):
formats = [formats]
include["format"] = formats

package.include.append(include)

if "exclude" in config:
package.exclude = config["exclude"]

if "packages" in config:
package.packages = config["packages"]

# Custom urls
if "urls" in config:
package.custom_urls = config["urls"]

return package

@classmethod
def create_dependency(
cls,
Expand Down
6 changes: 3 additions & 3 deletions src/poetry/core/masonry/builders/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def convert_entry_points(self) -> dict[str, list[str]]:
result = defaultdict(list)

# Scripts -> Entry points
for name, specification in self._poetry.local_config.get("scripts", {}).items():
for name, specification in self._poetry.package.scripts.items():
if isinstance(specification, str):
# TODO: deprecate this in favour or reference
specification = {"reference": specification, "type": "console"}
Expand Down Expand Up @@ -308,7 +308,7 @@ def convert_entry_points(self) -> dict[str, list[str]]:
result["console_scripts"].append(f"{name} = {reference}{extras}")

# Plugins -> entry points
plugins = self._poetry.local_config.get("plugins", {})
plugins = self._poetry.package.entrypoints
for groupname, group in plugins.items():
for name, specification in sorted(group.items()):
result[groupname].append(f"{name} = {specification}")
Expand All @@ -321,7 +321,7 @@ def convert_entry_points(self) -> dict[str, list[str]]:
def convert_script_files(self) -> list[Path]:
script_files: list[Path] = []

for name, specification in self._poetry.local_config.get("scripts", {}).items():
for name, specification in self._poetry.package.scripts.items():
if isinstance(specification, dict) and specification.get("type") == "file":
source = specification["reference"]

Expand Down
12 changes: 5 additions & 7 deletions src/poetry/core/masonry/builders/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@


if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Iterator
from tarfile import TarInfo

Expand Down Expand Up @@ -330,12 +329,11 @@ def find_files_to_add(self, exclude_build: bool = False) -> set[BuildIncludeFile
additional_files.add(Path("pyproject.toml"))

# add readme files if specified
if "readme" in self._poetry.local_config:
readme: str | Iterable[str] = self._poetry.local_config["readme"]
if isinstance(readme, str):
additional_files.add(Path(readme))
else:
additional_files.update(Path(r) for r in readme)
if self._poetry.package.readmes:
for readme in self._poetry.package.readmes:
additional_files.add(readme)
elif self._poetry.package.readme:
additional_files.add(self._poetry.package.readme)

for additional_file in additional_files:
file = BuildIncludeFile(
Expand Down
5 changes: 1 addition & 4 deletions src/poetry/core/masonry/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,7 @@ def prepare_metadata(self, metadata_directory: Path) -> Path:
dist_info = metadata_directory / self.dist_info
dist_info.mkdir(parents=True, exist_ok=True)

if (
"scripts" in self._poetry.local_config
or "plugins" in self._poetry.local_config
):
if self._poetry.package.scripts or self._poetry.package.entrypoints:
with (dist_info / "entry_points.txt").open(
"w", encoding="utf-8", newline="\n"
) as f:
Expand Down
24 changes: 6 additions & 18 deletions src/poetry/core/packages/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def __init__(
self.documentation_url: str | None = None
self.keywords: list[str] = []
self._license: License | None = None
self._readme: Path | None = None
self.readmes: tuple[Path, ...] = ()

self.extras: dict[NormalizedName, list[Dependency]] = {}
Expand Down Expand Up @@ -397,27 +398,14 @@ def category(self, category: str) -> None:

@property
def readme(self) -> Path | None:
warnings.warn(
(
"`readme` is deprecated: you are getting only the first readme file."
" Please use the plural form `readmes`."
),
DeprecationWarning,
stacklevel=2,
)
return next(iter(self.readmes), None)
if self._readme is None and self.readmes:
return next(iter(self.readmes), None)

return self._readme

@readme.setter
def readme(self, path: Path) -> None:
warnings.warn(
(
"`readme` is deprecated. Please assign a tuple to the plural form"
" `readmes`."
),
DeprecationWarning,
stacklevel=2,
)
self.readmes = (path,)
self._readme = path

@property
def yanked(self) -> bool:
Expand Down
3 changes: 3 additions & 0 deletions src/poetry/core/packages/project_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def __init__(
self.include: list[dict[str, Any]] = []
self.exclude: list[dict[str, Any]] = []
self.custom_urls: dict[str, str] = {}
self.scripts: dict[str, str | dict[str, Any]] = {}
self.gui_scripts: dict[str, str] = {}
self.entrypoints: dict[str, dict[str, str | dict[str, str]]] = {}

if self._python_versions == "*":
self._python_constraint = parse_constraint("~2.7 || >=3.4")
Expand Down
3 changes: 2 additions & 1 deletion src/poetry/core/packages/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from poetry.core.constraints.version import Version
from poetry.core.constraints.version import VersionRange
from poetry.core.constraints.version import parse_constraint
from poetry.core.pyproject.toml import PyProjectTOML
from poetry.core.version.markers import SingleMarkerLike
from poetry.core.version.markers import dnf

Expand Down Expand Up @@ -124,6 +123,8 @@ def is_python_project(path: Path) -> bool:
if not path.is_dir():
return False

from poetry.core.pyproject.toml import PyProjectTOML

setup_py = path / "setup.py"
setup_cfg = path / "setup.cfg"
setuptools_project = setup_py.exists() or setup_cfg.exists()
Expand Down
Empty file.
44 changes: 44 additions & 0 deletions src/poetry/core/pyproject/formats/content_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

from abc import ABC
from abc import abstractmethod
from typing import TYPE_CHECKING
from typing import Any


if TYPE_CHECKING:
from pathlib import Path

from poetry.core.packages.project_package import ProjectPackage
from poetry.core.pyproject.formats.validation_result import ValidationResult


class ContentFormat(ABC):
def __init__(self, content: dict[str, Any]) -> None:
self._content = content

@classmethod
@abstractmethod
def supports(cls, content: dict[str, Any]) -> bool:
...

@abstractmethod
def validate(self, strict: bool = False) -> ValidationResult:
...

@abstractmethod
def to_package(self, root: Path, with_groups: bool = True) -> ProjectPackage:
...

@property
@abstractmethod
def hash_content(self) -> dict[str, Any]:
...

@property
@abstractmethod
def poetry_config(self) -> dict[str, Any]:
"""
The custom poetry configuration (i.e. the parts in [tool.poetry] that are not related to the package)
"""
...
Loading

0 comments on commit 9b3eb32

Please sign in to comment.