Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor code to support multiple project type #567

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
203 changes: 50 additions & 153 deletions src/poetry/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@

from packaging.utils import canonicalize_name

from poetry.core.utils.helpers import combine_unicode
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
from poetry.core.pyproject.formats.content_format import ContentFormat
from poetry.core.pyproject.toml import PyProjectTOML

DependencyConstraint = Union[str, Dict[str, Any]]
DependencyConfig = Mapping[
Expand All @@ -39,186 +38,84 @@ class Factory:
Factory class to create various elements needed by Poetry.
"""

def __init__(self) -> None:
from poetry.core.pyproject.formats.legacy_content_format import (
LegacyContentFormat,
)
from poetry.core.pyproject.formats.standard_content_format import (
StandardContentFormat,
)
from poetry.core.pyproject.toml import PyProjectTOML

self._default_format: type[ContentFormat] = LegacyContentFormat
self._content_formats: dict[str, type[ContentFormat]] = {
"legacy": LegacyContentFormat,
"standard": StandardContentFormat,
}

self._pyproject_class = PyProjectTOML

def register_content_format(
self, name: str, content_format: type[ContentFormat]
) -> None:
self._content_formats[name] = content_format

def create_poetry(
self, cwd: Path | None = None, with_groups: bool = True
) -> Poetry:
from poetry.core.poetry import Poetry
from poetry.core.pyproject.toml import PyProjectTOML

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

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

# Checking validity
check_result = self.validate(local_config)
if check_result["errors"]:
check_result = pyproject.content.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 = pyproject.content.to_package(
root=poetry_file.parent, with_groups=with_groups
)

return Poetry(poetry_file, local_config, package)
return Poetry(poetry_file, pyproject, 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"]
)
def create_pyproject(self, path: Path) -> PyProjectTOML:
content_format = self.guess_content_format(path)

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

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

for dep in package.requires:
if dep.name == req.name:
dep.in_extras.append(extra_name)
package.extras[extra_name].append(dep)
def guess_content_format(self, path: Path) -> type[ContentFormat]:
if not path.exists():
return self._default_format

if "build" in config:
build = config["build"]
if not isinstance(build, dict):
build = {"script": build}
package.build_config = build or {}
from poetry.core.utils._compat import tomllib

if "include" in config:
package.include = []
data = tomllib.loads(path.read_text(encoding="utf-8"))

for include in config["include"]:
if not isinstance(include, dict):
include = {"path": include}
for name, fmt in self._content_formats.items():
if fmt.supports(data):
logger.debug("Using the %s format", name)
return fmt

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
raise RuntimeError(
"Unable to determine the content format of the pyproject.toml file"
)

@classmethod
def create_dependency(
Expand Down