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

feat: allow a folder with a list of wheels to be a repository #10017

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/poetry/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ def _get_environment_repositories() -> dict[str, dict[str, str]]:
"url": os.environ[env_key]
}

pattern = re.compile(r"POETRY_REPOSITORIES_(?P<name>[A-Z_]+)_PATH")

for env_key in os.environ:
match = pattern.match(env_key)
if match:
repositories[match.group("name").lower().replace("_", "-")] = {
"path": os.environ[env_key]
}

return repositories

@property
Expand Down
1 change: 1 addition & 0 deletions src/poetry/config/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
class Source:
name: str
url: str = ""
path: str = ""
priority: Priority = (
Priority.PRIMARY
) # cheating in annotation: str will be converted to Priority in __post_init__
Expand Down
12 changes: 9 additions & 3 deletions src/poetry/console/commands/source/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,17 @@ class SourceAddCommand(Command):
argument(
"url",
"Source repository URL."
" Required, except for PyPI, for which it is not allowed.",
"Required for http repositoy, except for PyPI, for which it is not allowed.",
optional=True,
),
]

options: ClassVar[list[Option]] = [
option(
"path",
"Source repository path.",
flag=False,
),
option(
"priority",
"p",
Expand All @@ -54,6 +59,7 @@ def handle(self) -> int:
name: str = self.argument("name")
lower_name = name.lower()
url: str = self.argument("url")
path: str = self.option("path", "")
priority_str: str | None = self.option("priority", None)

if lower_name == "pypi":
Expand All @@ -63,7 +69,7 @@ def handle(self) -> int:
"<error>The URL of PyPI is fixed and cannot be set.</error>"
)
return 1
elif not url:
elif not url and not path:
self.line_error(
"<error>A custom source cannot be added without a URL.</error>"
)
Expand All @@ -75,7 +81,7 @@ def handle(self) -> int:
priority = Priority[priority_str.upper()]

sources = AoT([])
new_source = Source(name=name, url=url, priority=priority)
new_source = Source(name=name, url=url, path=path, priority=priority)
is_new_source = True

for source in self.poetry.get_sources():
Expand Down
35 changes: 22 additions & 13 deletions src/poetry/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ def create_poetry(
url = source.get("url")
if name and url and name not in existing_repositories:
repositories[name] = {"url": url}
path = source.get("path")
if name and path and name not in existing_repositories:
repositories[name] = {"path": path}

config.merge({"repositories": repositories})

Expand Down Expand Up @@ -146,7 +149,7 @@ def create_pool(

if io.is_debug():
io.write_line(
f"Adding repository {repository.name} ({repository.url})"
f"Adding repository {repository.name} ({getattr(repository, 'url', repository.path)})"
f" and setting it as {priority.name.lower()}"
)

Expand Down Expand Up @@ -182,6 +185,7 @@ def create_package_source(
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.single_page_repository import SinglePageRepository
from poetry.repositories.wheelhouse_repository import LocalPathRepository

try:
name = source["name"]
Expand All @@ -204,20 +208,25 @@ def create_package_source(
try:
url = source["url"]
except KeyError:
raise InvalidSourceError(f"Missing [url] in source {name!r}.")

repository_class = LegacyRepository
try:
path = source["path"]
except KeyError:
raise InvalidSourceError(f"Missing [url] or [path] in source {name!r}.")
else:
return LocalPathRepository(name, path)
Comment on lines 208 to +216
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider validating that only one of 'url' or 'path' is provided in source configuration

When both 'url' and 'path' are provided, the code silently ignores 'path'. Consider adding validation to explicitly handle this case and provide clear feedback to users.

else:
repository_class = LegacyRepository

if re.match(r".*\.(htm|html)$", url):
repository_class = SinglePageRepository
if re.match(r".*\.(htm|html)$", url):
repository_class = SinglePageRepository

return repository_class(
name,
url,
config=config,
disable_cache=disable_cache,
pool_size=pool_size,
)
return repository_class(
name,
url,
config=config,
disable_cache=disable_cache,
pool_size=pool_size,
)

@classmethod
def create_pyproject_from_package(cls, package: Package) -> TOMLDocument:
Expand Down
4 changes: 4 additions & 0 deletions src/poetry/json/schemas/poetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"description": "The url of the repository.",
"format": "uri"
},
"path": {
"type": "string",
"description": "The path of the repository."
},
"priority": {
"enum": [
"primary",
Expand Down
39 changes: 39 additions & 0 deletions src/poetry/repositories/wheelhouse_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import logging
from typing import Iterator
from pathlib import Path

from cachecontrol.controller import logger as cache_control_logger
from poetry.core.packages.package import Package
from poetry.core.packages.utils.link import Link
from poetry.repositories.repository import Repository
from poetry.inspection.info import PackageInfo
from poetry.core.packages.utils.utils import path_to_url

cache_control_logger.setLevel(logging.ERROR)

logger = logging.getLogger(__name__)


class LocalPathRepository(Repository):
def __init__(self, name: str, path: str) -> None:
self._path = Path(path)
if not self._path.is_dir():
raise ValueError(f"Path {self._path} is not a directory")

super().__init__(name, self._list_packages_in_path())

@property
def path(self) -> str:
return self._path

def _list_packages_in_path(self) -> Iterator[Package]:
for file_path in self._path.iterdir():
try:
yield PackageInfo.from_path(path=file_path).to_package(root_dir=file_path)
except Exception:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Avoid catching bare Exception without logging

Catching all exceptions silently could hide important errors. Consider logging the exceptions to help with debugging and potentially catch more specific exceptions.

Suggested implementation:

            try:
                yield PackageInfo.from_path(path=file_path).to_package(root_dir=file_path)
            except (ValueError, OSError) as e:
                logger.warning(
                    "Failed to load package info from %s: %s", file_path, str(e)
                )
                continue

You'll also need to add a logger instance at the top of the file if it doesn't exist already:

logger = logging.getLogger(__name__)

The exceptions I've chosen (ValueError, OSError) are common ones that might occur when reading package info, but you may want to adjust these based on the specific exceptions that PackageInfo.from_path() and to_package() can raise.

continue

def find_links_for_package(self, package: Package) -> list[Link]:
return [Link(path_to_url(package.source_type))]
Comment on lines +38 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The find_links_for_package method appears to be using the wrong attribute for path generation

Using package.source_type (which is typically a string like 'directory' or 'file') as a path will likely cause errors. Consider using package.source_url or constructing the correct path from self._path and the package filename.

Loading