def test_installer_required_extras_should_be_installed( locker, repo, package, installed, env, mocker ): pool = Pool() pool.add_repository(MockRepository()) installer = Installer(NullIO(), env, package, locker, pool, installed=installed) package.add_dependency( "cachecontrol", {"version": "^0.12.5", "extras": ["filecache"]} ) installer.update(True) installer.run() assert len(installer.installer.installs) == 2 assert len(installer.installer.updates) == 0 assert len(installer.installer.removals) == 0 locker.locked(True) locker.mock_lock_data(locker.written_data) installer = Installer(NullIO(), env, package, locker, pool, installed=installed) installer.update(True) installer.run() assert len(installer.installer.installs) == 2 assert len(installer.installer.updates) == 0 assert len(installer.installer.removals) == 0
def test_pool_raises_connection_error_when_offline(): pool = Pool() pool.add_repository( LegacyRepository(url="http://fake.url/simple", name="fake")) with pytest.raises(ConnectionError): pool.package("foo", "1.0.0")
def _factory( name=None, dependencies=None, dev_dependencies=None, pyproject_content=None, poetry_lock_content=None, install_deps=True, ): project_dir = workspace / "poetry-fixture-{}".format(name) dependencies = dependencies or {} dev_dependencies = dev_dependencies or {} if pyproject_content: project_dir.mkdir(parents=True, exist_ok=True) with project_dir.joinpath("pyproject.toml").open( "w", encoding="utf-8" ) as f: f.write(pyproject_content) else: layout("src")( name, "0.1.0", author="PyTest Tester <*****@*****.**>", readme_format="md", python=default_python, dependencies=dependencies, dev_dependencies=dev_dependencies, ).create(project_dir, with_tests=False) if poetry_lock_content: lock_file = project_dir / "poetry.lock" lock_file.write_text(data=poetry_lock_content, encoding="utf-8") poetry = Factory().create_poetry(project_dir) locker = TestLocker( poetry.locker.lock.path, poetry.locker._local_config ) # noqa locker.write() poetry.set_locker(locker) poetry.set_config(config) pool = Pool() pool.add_repository(repo) poetry.set_pool(pool) if install_deps: for deps in [dependencies, dev_dependencies]: for name, version in deps.items(): pkg = get_package(name, version) repo.add_package(pkg) installed.add_package(pkg) return poetry
def test_installer_can_handle_old_lock_files(installer, locker, package, repo, installed, config): pool = Pool() pool.add_repository(MockRepository()) package.add_dependency("pytest", "^3.5", category="dev") locker.locked() locker.mock_lock_data(fixture("old-lock")) installer = Installer(NullIO(), MockEnv(), package, locker, pool, config, installed=installed) installer.run() assert 6 == len(installer.installer.installs) installer = Installer( NullIO(), MockEnv(version_info=(2, 7, 18)), package, locker, pool, config, installed=installed, ) installer.run() # funcsigs will be added assert 7 == len(installer.installer.installs) installer = Installer( NullIO(), MockEnv(version_info=(2, 7, 18), platform="win32"), package, locker, pool, config, installed=installed, ) installer.run() # colorama will be added assert 8 == len(installer.installer.installs)
def test_installer_with_pypi_repository(package, locker, installed): pool = Pool() pool.add_repository(MockRepository()) installer = Installer( NullIO(), NullEnv(), package, locker, pool, installed=installed ) package.add_dependency("pytest", "^3.5", category="dev") installer.run() expected = fixture("with-pypi-repository") assert locker.written_data == expected
def test_installer_with_pypi_repository(package, locker, installed): pool = Pool() pool.add_repository(MockRepository()) installer = Installer( NullIO(), NullVenv(), package, locker, pool, installed=installed ) package.add_dependency("pytest", "^3.5", category="dev") installer.run() expected = fixture("with-pypi-repository") assert locker.written_data == expected
def test_installer_required_extras_should_not_be_removed_when_updating_single_dependency_pypi_repository( locker, repo, package, installed, env, mocker, config): mocker.patch("sys.platform", "darwin") pool = Pool() pool.add_repository(MockRepository()) installer = Installer(NullIO(), env, package, locker, pool, config, installed=installed) package.add_dependency("poetry", {"version": "^0.12.0"}) installer.update(True) installer.run() assert len(installer.installer.installs) == 3 assert len(installer.installer.updates) == 0 assert len(installer.installer.removals) == 0 package.add_dependency("pytest", "^3.5") locker.locked(True) locker.mock_lock_data(locker.written_data) for pkg in installer.installer.installs: installed.add_package(pkg) installer = Installer(NullIO(), env, package, locker, pool, config, installed=installed) installer.update(True) installer.whitelist(["pytest"]) installer.run() assert len(installer.installer.installs) == 6 if not PY2 else 7 assert len(installer.installer.updates) == 0 assert len(installer.installer.removals) == 0
def test_installer_with_pypi_repository(package, locker, installed): pool = Pool() pool.add_repository(MockRepository()) installer = Installer(NullIO(), NullVenv(), package, locker, pool, installed=installed) package.add_dependency('pytest', '^3.5', category='dev') installer.run() expected = fixture('with-pypi-repository') assert locker.written_data == expected
def test_pool_fallback_through_repos(): pool = Pool() pool.add_repository( LegacyRepository(url="http://fake.url/simple", name="fake")) pool.add_repository(MockRepository()) package = pool.package("requests", "2.18.4") assert package.name == "requests" assert len(package.requires) == 4 assert len(package.extras["security"]) == 3 assert len(package.extras["socks"]) == 2 win_inet = package.extras["socks"][0] assert win_inet.name == "win-inet-pton" assert win_inet.python_versions == "~2.7 || ~2.6" assert str(win_inet.marker) == ( 'sys_platform == "win32" and (python_version == "2.7" ' 'or python_version == "2.6") and extra == "socks"')
def poetry(repo, project_directory, config): p = Factory().create_poetry( Path(__file__).parent.parent / "fixtures" / project_directory) p.set_locker(TestLocker(p.locker.lock.path, p.locker._local_config)) with p.file.path.open(encoding="utf-8") as f: content = f.read() p.set_config(config) pool = Pool() pool.add_repository(repo) p.set_pool(pool) yield p with p.file.path.open("w", encoding="utf-8") as f: f.write(content)
def test_installer_can_install_dependencies_from_forced_source( locker, package, installed, env ): package.python_versions = "^3.7" package.add_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) pool = Pool() pool.add_repository(MockLegacyRepository()) pool.add_repository(MockRepository()) installer = Installer(NullIO(), env, package, locker, pool, installed=installed) installer.update(True) installer.run() assert len(installer.installer.installs) == 1 assert len(installer.installer.updates) == 0 assert len(installer.installer.removals) == 0
def test_repository_with_normal_default_and_secondary_repositories(): secondary = LegacyRepository("secondary", "https://secondary.com") default = LegacyRepository("default", "https://default.com") repo1 = LegacyRepository("foo", "https://foo.bar") repo2 = LegacyRepository("bar", "https://bar.baz") pool = Pool() pool.add_repository(repo1) pool.add_repository(secondary, secondary=True) pool.add_repository(repo2) pool.add_repository(default, default=True) assert pool.repository("secondary") is secondary assert pool.repository("default") is default assert pool.repository("foo") is repo1 assert pool.repository("bar") is repo2 assert pool.has_default()
def test_repository_from_secondary_pool(): repo = LegacyRepository("foo", "https://foo.bar") pool = Pool() pool.add_repository(repo, secondary=True) assert pool.repository("foo") is repo
def test_repository_from_normal_pool(): repo = LegacyRepository("foo", "https://foo.bar") pool = Pool() pool.add_repository(repo) assert pool.repository("foo") is repo
def _factory( name: str | None = None, dependencies: dict[str, str] | None = None, dev_dependencies: dict[str, str] | None = None, pyproject_content: str | None = None, poetry_lock_content: str | None = None, install_deps: bool = True, source: Path | None = None, locker_config: dict[str, Any] | None = None, ) -> Poetry: project_dir = workspace / f"poetry-fixture-{name}" dependencies = dependencies or {} dev_dependencies = dev_dependencies or {} if pyproject_content or source: if source: project_dir.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(source, project_dir) else: project_dir.mkdir(parents=True, exist_ok=True) if pyproject_content: with project_dir.joinpath("pyproject.toml").open( "w", encoding="utf-8") as f: f.write(pyproject_content) else: layout("src")( name, "0.1.0", author="PyTest Tester <*****@*****.**>", readme_format="md", python=default_python, dependencies=dependencies, dev_dependencies=dev_dependencies, ).create(project_dir, with_tests=False) if poetry_lock_content: lock_file = project_dir / "poetry.lock" lock_file.write_text(data=poetry_lock_content, encoding="utf-8") poetry = Factory().create_poetry(project_dir) locker = TestLocker(poetry.locker.lock.path, locker_config or poetry.locker._local_config) locker.write() poetry.set_locker(locker) poetry.set_config(config) pool = Pool() pool.add_repository(repo) poetry.set_pool(pool) if install_deps: for deps in [dependencies, dev_dependencies]: for name, version in deps.items(): pkg = get_package(name, version) repo.add_package(pkg) installed.add_package(pkg) return poetry
def _do_install(self, local_repo: Repository) -> int: from poetry.puzzle import Solver locked_repository = Repository() if self._update: if self._locker.is_locked() and not self._lock: locked_repository = self._locker.locked_repository() # If no packages have been whitelisted (The ones we want to update), # we whitelist every package in the lock file. if not self._whitelist: for pkg in locked_repository.packages: self._whitelist.append(pkg.name) # Checking extras for extra in self._extras: if extra not in self._package.extras: raise ValueError(f"Extra [{extra}] is not specified.") self._io.write_line("<info>Updating dependencies</>") solver = Solver( self._package, self._pool, self._installed_repository, locked_repository, self._io, ) with solver.provider.use_source_root( source_root=self._env.path.joinpath("src") ): ops = solver.solve(use_latest=self._whitelist).calculate_operations() else: self._io.write_line("<info>Installing dependencies from lock file</>") locked_repository = self._locker.locked_repository() if not self._locker.is_fresh(): self._io.write_error_line( "<warning>" "Warning: poetry.lock is not consistent with pyproject.toml. " "You may be getting improper dependencies. " "Run `poetry lock [--no-update]` to fix it." "</warning>" ) for extra in self._extras: if extra not in self._locker.lock_data.get("extras", {}): raise ValueError(f"Extra [{extra}] is not specified.") # If we are installing from lock # Filter the operations by comparing it with what is # currently installed ops = self._get_operations_from_lock(locked_repository) self._populate_local_repo(local_repo, ops) if self._update: self._write_lock_file(local_repo) if self._lock: # If we are only in lock mode, no need to go any further return 0 if self._groups is not None: root = self._package.with_dependency_groups(list(self._groups), only=True) else: root = self._package.without_optional_dependency_groups() if self._io.is_verbose(): self._io.write_line("") self._io.write_line( "<info>Finding the necessary packages for the current system</>" ) # We resolve again by only using the lock file pool = Pool(ignore_repository_names=True) # Making a new repo containing the packages # newly resolved and the ones from the current lock file repo = Repository() for package in local_repo.packages + locked_repository.packages: if not repo.has_package(package): repo.add_package(package) pool.add_repository(repo) solver = Solver( root, pool, self._installed_repository, locked_repository, NullIO() ) # Everything is resolved at this point, so we no longer need # to load deferred dependencies (i.e. VCS, URL and path dependencies) solver.provider.load_deferred(False) with solver.use_environment(self._env): ops = solver.solve(use_latest=self._whitelist).calculate_operations( with_uninstalls=self._requires_synchronization, synchronize=self._requires_synchronization, ) if not self._requires_synchronization: # If no packages synchronisation has been requested we need # to calculate the uninstall operations from poetry.puzzle.transaction import Transaction transaction = Transaction( locked_repository.packages, [(package, 0) for package in local_repo.packages], installed_packages=self._installed_repository.packages, root_package=root, ) ops = [ op for op in transaction.calculate_operations(with_uninstalls=True) if op.job_type == "uninstall" ] + ops # We need to filter operations so that packages # not compatible with the current system, # or optional and not requested, are dropped self._filter_operations(ops, local_repo) # Execute operations return self._execute(ops)
class InitCommand(Command): """ Creates a basic <comment>pyproject.toml</> file in the current directory. init {--name= : Name of the package} {--description= : Description of the package} {--author= : Author name of the package} {--dependency=* : Package to require with an optional version constraint, e.g. requests:^2.10.0 or requests=2.11.1} {--dev-dependency=* : Package to require for development with an optional version constraint, e.g. requests:^2.10.0 or requests=2.11.1} {--l|license= : License of the package} """ help = """\ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in the current directory. """ def __init__(self): super(InitCommand, self).__init__() self._pool = None def handle(self): from poetry.layouts import layout from poetry.utils._compat import Path from poetry.utils.env import Env from poetry.vcs.git import GitConfig if (Path.cwd() / "pyproject.toml").exists(): self.error("A pyproject.toml file already exists.") return 1 vcs_config = GitConfig() self.line([ "", "This command will guide you through creating your <info>pyproject.toml</> config.", "", ]) name = self.option("name") if not name: name = Path.cwd().name.lower() question = self.create_question( "Package name [<comment>{}</comment>]: ".format(name), default=name) name = self.ask(question) version = "0.1.0" question = self.create_question( "Version [<comment>{}</comment>]: ".format(version), default=version) version = self.ask(question) description = self.option("description") or "" question = self.create_question( "Description [<comment>{}</comment>]: ".format(description), default=description, ) description = self.ask(question) author = self.option("author") if not author and vcs_config and vcs_config.get("user.name"): author = vcs_config["user.name"] author_email = vcs_config.get("user.email") if author_email: author += " <{}>".format(author_email) question = self.create_question( "Author [<comment>{}</comment>, n to skip]: ".format(author), default=author) question.validator = lambda v: self._validate_author(v, author) author = self.ask(question) if not author: authors = [] else: authors = [author] license = self.option("license") or "" question = self.create_question( "License [<comment>{}</comment>]: ".format(license), default=license) question.validator = self._validate_license license = self.ask(question) current_env = Env.get() default_python = "^{}".format(".".join( str(v) for v in current_env.version_info[:2])) question = self.create_question( "Compatible Python versions [<comment>{}</comment>]: ".format( default_python), default=default_python, ) python = self.ask(question) self.line("") requirements = {} question = ("Would you like to define your dependencies" " (require) interactively?") if self.confirm(question, True): requirements = self._format_requirements( self._determine_requirements(self.option("dependency"))) dev_requirements = {} question = ("Would you like to define your dev dependencies" " (require-dev) interactively") if self.confirm(question, True): dev_requirements = self._format_requirements( self._determine_requirements(self.option("dev-dependency"))) layout_ = layout("standard")( name, version, description=description, author=authors[0] if authors else None, license=license, python=python, dependencies=requirements, dev_dependencies=dev_requirements, ) content = layout_.generate_poetry_content() if self.input.is_interactive(): self.line("<info>Generated file</info>") self.line(["", content, ""]) if not self.confirm("Do you confirm generation?", True): self.line("<error>Command aborted</error>") return 1 with (Path.cwd() / "pyproject.toml").open("w") as f: f.write(content) def _determine_requirements( self, requires, allow_prereleases=False # type: List[str] # type: bool ): # type: (...) -> List[str] if not requires: requires = [] package = self.ask("Search for package:") while package is not None: matches = self._get_pool().search(package) if not matches: self.line("<error>Unable to find package</error>") package = False else: choices = [] for found_package in matches: choices.append(found_package.pretty_name) self.line( "Found <info>{}</info> packages matching <info>{}</info>" .format(len(matches), package)) package = self.choice( "\nEnter package # to add, or the complete package name if it is not listed", choices, attempts=3, ) # no constraint yet, determine the best version automatically if package is not False and " " not in package: question = self.create_question( "Enter the version constraint to require " "(or leave blank to use the latest version):") question.attempts = 3 question.validator = lambda x: (x or "").strip() or False constraint = self.ask(question) if constraint is False: _, constraint = self._find_best_version_for_package( package) self.line( "Using version <info>{}</info> for <info>{}</info>" .format(constraint, package)) package += " {}".format(constraint) if package is not False: requires.append(package) package = self.ask("\nSearch for a package:") return requires requires = self._parse_name_version_pairs(requires) result = [] for requirement in requires: if "version" not in requirement: # determine the best version automatically name, version = self._find_best_version_for_package( requirement["name"], allow_prereleases=allow_prereleases) requirement["version"] = version requirement["name"] = name self.line("Using version <info>{}</> for <info>{}</>".format( version, name)) else: # check that the specified version/constraint exists # before we proceed name, _ = self._find_best_version_for_package( requirement["name"], requirement["version"], allow_prereleases=allow_prereleases, ) requirement["name"] = name result.append("{} {}".format(requirement["name"], requirement["version"])) return result def _find_best_version_for_package( self, name, required_version=None, allow_prereleases=False): # type: (...) -> Tuple[str, str] from poetry.version.version_selector import VersionSelector selector = VersionSelector(self._get_pool()) package = selector.find_best_candidate( name, required_version, allow_prereleases=allow_prereleases) if not package: # TODO: find similar raise ValueError( "Could not find a matching version of package {}".format(name)) return (package.pretty_name, selector.find_recommended_require_version(package)) def _parse_name_version_pairs(self, pairs): # type: (list) -> list result = [] for i in range(len(pairs)): pair = re.sub("^([^=: ]+)[=: ](.*)$", "\\1 \\2", pairs[i].strip()) pair = pair.strip() if " " in pair: name, version = pair.split(" ", 2) result.append({"name": name, "version": version}) else: result.append({"name": pair}) return result def _format_requirements(self, requirements): # type: (List[str]) -> dict requires = {} requirements = self._parse_name_version_pairs(requirements) for requirement in requirements: requires[requirement["name"]] = requirement["version"] return requires def _validate_author(self, author, default): from poetry.packages.package import AUTHOR_REGEX author = author or default if author in ["n", "no"]: return m = AUTHOR_REGEX.match(author) if not m: raise ValueError("Invalid author string. Must be in the format: " "John Smith <*****@*****.**>") return author def _validate_license(self, license): from poetry.spdx import license_by_id if license: license_by_id(license) return license def _get_pool(self): from poetry.repositories import Pool from poetry.repositories.pypi_repository import PyPiRepository if isinstance(self, EnvCommand): return self.poetry.pool if self._pool is None: self._pool = Pool() self._pool.add_repository(PyPiRepository()) return self._pool
def pool(repo): pool = Pool() pool.add_repository(repo) return pool
def _do_install(self, local_repo: Repository) -> int: from poetry.puzzle import Solver locked_repository = Repository() if self._update: if self._locker.is_locked() and not self._lock: locked_repository = self._locker.locked_repository(True) # If no packages have been whitelisted (The ones we want to update), # we whitelist every package in the lock file. if not self._whitelist: for pkg in locked_repository.packages: self._whitelist.append(pkg.name) # Checking extras for extra in self._extras: if extra not in self._package.extras: raise ValueError(f"Extra [{extra}] is not specified.") self._io.write_line("<info>Updating dependencies</>") solver = Solver( self._package, self._pool, self._installed_repository, locked_repository, self._io, remove_untracked=self._remove_untracked, ) ops = solver.solve(use_latest=self._whitelist) else: self._io.write_line( "<info>Installing dependencies from lock file</>") locked_repository = self._locker.locked_repository(True) if not self._locker.is_fresh(): self._io.write_line( "<warning>" "Warning: The lock file is not up to date with " "the latest changes in pyproject.toml. " "You may be getting outdated dependencies. " "Run update to update them." "</warning>") for extra in self._extras: if extra not in self._locker.lock_data.get("extras", {}): raise ValueError(f"Extra [{extra}] is not specified.") # If we are installing from lock # Filter the operations by comparing it with what is # currently installed ops = self._get_operations_from_lock(locked_repository) self._populate_local_repo(local_repo, ops) if self._update: self._write_lock_file(local_repo) if self._lock: # If we are only in lock mode, no need to go any further return 0 if self._without_groups or self._with_groups or self._only_groups: if self._with_groups: # Default dependencies and opted-in optional dependencies root = self._package.with_dependency_groups(self._with_groups) elif self._without_groups: # Default dependencies without selected groups root = self._package.without_dependency_groups( self._without_groups) else: # Only selected groups root = self._package.with_dependency_groups(self._only_groups, only=True) else: root = self._package.without_optional_dependency_groups() if self._io.is_verbose(): self._io.write_line("") self._io.write_line( "<info>Finding the necessary packages for the current system</>" ) # We resolve again by only using the lock file pool = Pool(ignore_repository_names=True) # Making a new repo containing the packages # newly resolved and the ones from the current lock file repo = Repository() for package in local_repo.packages + locked_repository.packages: if not repo.has_package(package): repo.add_package(package) pool.add_repository(repo) solver = Solver( root, pool, self._installed_repository, locked_repository, NullIO(), remove_untracked=self._remove_untracked, ) # Everything is resolved at this point, so we no longer need # to load deferred dependencies (i.e. VCS, URL and path dependencies) solver.provider.load_deferred(False) with solver.use_environment(self._env): ops = solver.solve(use_latest=self._whitelist) # We need to filter operations so that packages # not compatible with the current system, # or optional and not requested, are dropped self._filter_operations(ops, local_repo) # Execute operations return self._execute(ops)
def pool(repo: TestRepository) -> Pool: pool = Pool() pool.add_repository(repo) return pool
class InitCommand(Command): name = "init" description = ( "Creates a basic <comment>pyproject.toml</> file in the current directory." ) options = [ option("name", None, "Name of the package.", flag=False), option("description", None, "Description of the package.", flag=False), option("author", None, "Author name of the package.", flag=False), option("python", None, "Compatible Python versions.", flag=False), option( "dependency", None, "Package to require, with an optional version constraint, " "e.g. requests:^2.10.0 or requests=2.11.1.", flag=False, multiple=True, ), option( "dev-dependency", None, "Package to require for development, with an optional version constraint, " "e.g. requests:^2.10.0 or requests=2.11.1.", flag=False, multiple=True, ), option("license", "l", "License of the package.", flag=False), ] help = """\ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the\ current directory. """ def __init__(self) -> None: super().__init__() self._pool: Pool | None = None def handle(self) -> int: from pathlib import Path from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.vcs.git import GitConfig from poetry.layouts import layout from poetry.utils.env import SystemEnv pyproject = PyProjectTOML(Path.cwd() / "pyproject.toml") if pyproject.file.exists(): if pyproject.is_poetry_project(): self.line_error( "<error>A pyproject.toml file with a poetry section already" " exists.</error>") return 1 if pyproject.data.get("build-system"): self.line_error( "<error>A pyproject.toml file with a defined build-system already" " exists.</error>") return 1 vcs_config = GitConfig() if self.io.is_interactive(): self.line("") self.line("This command will guide you through creating your" " <info>pyproject.toml</> config.") self.line("") name = self.option("name") if not name: name = Path.cwd().name.lower() question = self.create_question( f"Package name [<comment>{name}</comment>]: ", default=name) name = self.ask(question) version = "0.1.0" question = self.create_question( f"Version [<comment>{version}</comment>]: ", default=version) version = self.ask(question) description = self.option("description") or "" question = self.create_question( f"Description [<comment>{description}</comment>]: ", default=description, ) description = self.ask(question) author = self.option("author") if not author and vcs_config.get("user.name"): author = vcs_config["user.name"] author_email = vcs_config.get("user.email") if author_email: author += f" <{author_email}>" question = self.create_question( f"Author [<comment>{author}</comment>, n to skip]: ", default=author) question.set_validator(lambda v: self._validate_author(v, author)) author = self.ask(question) if not author: authors = [] else: authors = [author] license = self.option("license") or "" question = self.create_question( f"License [<comment>{license}</comment>]: ", default=license) question.set_validator(self._validate_license) license = self.ask(question) python = self.option("python") if not python: current_env = SystemEnv(Path(sys.executable)) default_python = "^" + ".".join( str(v) for v in current_env.version_info[:2]) question = self.create_question( f"Compatible Python versions [<comment>{default_python}</comment>]: ", default=default_python, ) python = self.ask(question) if self.io.is_interactive(): self.line("") requirements: Requirements = {} if self.option("dependency"): requirements = self._format_requirements( self._determine_requirements(self.option("dependency"))) question = "Would you like to define your main dependencies interactively?" help_message = """\ You can specify a package in the following forms: - A single name (<b>requests</b>) - A name and a constraint (<b>requests@^2.23.0</b>) - A git url (<b>git+https://github.com/python-poetry/poetry.git</b>) - A git url with a revision\ (<b>git+https://github.com/python-poetry/poetry.git#develop</b>) - A file path (<b>../my-package/my-package.whl</b>) - A directory (<b>../my-package/</b>) - A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>) """ help_displayed = False if self.confirm(question, True): if self.io.is_interactive(): self.line(help_message) help_displayed = True requirements.update( self._format_requirements(self._determine_requirements([]))) if self.io.is_interactive(): self.line("") dev_requirements: Requirements = {} if self.option("dev-dependency"): dev_requirements = self._format_requirements( self._determine_requirements(self.option("dev-dependency"))) question = ( "Would you like to define your development dependencies interactively?" ) if self.confirm(question, True): if self.io.is_interactive() and not help_displayed: self.line(help_message) dev_requirements.update( self._format_requirements(self._determine_requirements([]))) if self.io.is_interactive(): self.line("") layout_ = layout("standard")( name, version, description=description, author=authors[0] if authors else None, license=license, python=python, dependencies=requirements, dev_dependencies=dev_requirements, ) content = layout_.generate_poetry_content(original=pyproject) if self.io.is_interactive(): self.line("<info>Generated file</info>") self.line("") self.line(content) self.line("") if not self.confirm("Do you confirm generation?", True): self.line_error("<error>Command aborted</error>") return 1 with (Path.cwd() / "pyproject.toml").open("w", encoding="utf-8") as f: f.write(content) return 0 def _generate_choice_list(self, matches: list[Package], canonicalized_name: str) -> list[str]: choices = [] matches_names = [p.name for p in matches] exact_match = canonicalized_name in matches_names if exact_match: choices.append( matches[matches_names.index(canonicalized_name)].pretty_name) for found_package in matches: if len(choices) >= 10: break if found_package.name == canonicalized_name: continue choices.append(found_package.pretty_name) return choices def _determine_requirements( self, requires: list[str], allow_prereleases: bool = False, source: str | None = None, ) -> list[dict[str, Any]]: if not requires: result = [] package = self.ask( "Search for package to add (or leave blank to continue):") while package: constraint = self._parse_requirements([package])[0] if ("git" in constraint or "url" in constraint or "path" in constraint or "version" in constraint): self.line(f"Adding <info>{package}</info>") result.append(constraint) package = self.ask("\nAdd a package:") continue canonicalized_name = canonicalize_name(constraint["name"]) matches = self._get_pool().search(canonicalized_name) if not matches: self.line_error("<error>Unable to find package</error>") package = False else: choices = self._generate_choice_list( matches, canonicalized_name) info_string = ( f"Found <info>{len(matches)}</info> packages matching" f" <c1>{package}</c1>") if len(matches) > 10: info_string += "\nShowing the first 10 matches" self.line(info_string) # Default to an empty value to signal no package was selected choices.append("") package = self.choice( "\nEnter package # to add, or the complete package name if it" " is not listed", choices, attempts=3, default=len(choices) - 1, ) if not package: self.line("<warning>No package selected</warning>") # package selected by user, set constraint name to package name if package: constraint["name"] = package # no constraint yet, determine the best version automatically if package and "version" not in constraint: question = self.create_question( "Enter the version constraint to require " "(or leave blank to use the latest version):") question.attempts = 3 question.validator = lambda x: (x or "").strip() or False package_constraint = self.ask(question) if package_constraint is None: _, package_constraint = self._find_best_version_for_package( package) self.line( f"Using version <b>{package_constraint}</b> for" f" <c1>{package}</c1>") constraint["version"] = package_constraint if package: result.append(constraint) if self.io.is_interactive(): package = self.ask("\nAdd a package:") return result result = [] for requirement in self._parse_requirements(requires): if "git" in requirement or "url" in requirement or "path" in requirement: result.append(requirement) continue elif "version" not in requirement: # determine the best version automatically name, version = self._find_best_version_for_package( requirement["name"], allow_prereleases=allow_prereleases, source=source, ) requirement["version"] = version requirement["name"] = name self.line( f"Using version <b>{version}</b> for <c1>{name}</c1>") else: # check that the specified version/constraint exists # before we proceed name, _ = self._find_best_version_for_package( requirement["name"], requirement["version"], allow_prereleases=allow_prereleases, source=source, ) requirement["name"] = name result.append(requirement) return result def _find_best_version_for_package( self, name: str, required_version: str | None = None, allow_prereleases: bool = False, source: str | None = None, ) -> tuple[str, str]: from poetry.version.version_selector import VersionSelector selector = VersionSelector(self._get_pool()) package = selector.find_best_candidate( name, required_version, allow_prereleases=allow_prereleases, source=source) if not package: # TODO: find similar raise ValueError( f"Could not find a matching version of package {name}") return package.pretty_name, selector.find_recommended_require_version( package) def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: from poetry.core.pyproject.exceptions import PyProjectException try: cwd = self.poetry.file.parent except (PyProjectException, RuntimeError): cwd = Path.cwd() return [ parse_dependency_specification( requirement=requirement, env=self.env if isinstance(self, EnvCommand) else None, cwd=cwd, ) for requirement in requirements ] def _format_requirements( self, requirements: list[dict[str, str]]) -> Requirements: requires: Requirements = {} for requirement in requirements: name = requirement.pop("name") constraint: str | InlineTable if "version" in requirement and len(requirement) == 1: constraint = requirement["version"] else: constraint = inline_table() constraint.trivia.trail = "\n" constraint.update(requirement) requires[name] = constraint return requires def _validate_author(self, author: str, default: str) -> str | None: from poetry.core.packages.package import AUTHOR_REGEX author = author or default if author in ["n", "no"]: return None m = AUTHOR_REGEX.match(author) if not m: raise ValueError("Invalid author string. Must be in the format: " "John Smith <*****@*****.**>") return author def _validate_license(self, license: str) -> str: from poetry.core.spdx.helpers import license_by_id if license: license_by_id(license) return license def _get_pool(self) -> Pool: from poetry.repositories import Pool from poetry.repositories.pypi_repository import PyPiRepository if isinstance(self, EnvCommand): return self.poetry.pool if self._pool is None: self._pool = Pool() self._pool.add_repository(PyPiRepository()) return self._pool
def _do_install(self, local_repo): locked_repository = Repository() if self._update: if self._locker.is_locked(): locked_repository = self._locker.locked_repository(True) # If no packages have been whitelisted (The ones we want to update), # we whitelist every package in the lock file. if not self._whitelist: for pkg in locked_repository.packages: self._whitelist.append(pkg.name) # Checking extras for extra in self._extras: if extra not in self._package.extras: raise ValueError("Extra [{}] is not specified.".format(extra)) self._io.writeln("<info>Updating dependencies</>") solver = Solver( self._package, self._pool, self._installed_repository, locked_repository, self._io, ) ops = solver.solve(use_latest=self._whitelist) else: self._io.writeln("<info>Installing dependencies from lock file</>") locked_repository = self._locker.locked_repository(True) if not self._locker.is_fresh(): self._io.writeln( "<warning>" "Warning: The lock file is not up to date with " "the latest changes in pyproject.toml. " "You may be getting outdated dependencies. " "Run update to update them." "</warning>" ) for extra in self._extras: if extra not in self._locker.lock_data.get("extras", {}): raise ValueError("Extra [{}] is not specified.".format(extra)) # If we are installing from lock # Filter the operations by comparing it with what is # currently installed ops = self._get_operations_from_lock(locked_repository) self._populate_local_repo(local_repo, ops, locked_repository) with self._package.with_python_versions( ".".join([str(i) for i in self._venv.version_info[:3]]) ): # We resolve again by only using the lock file pool = Pool() # Making a new repo containing the packages # newly resolved and the ones from the current lock file locked_repository = self._locker.locked_repository(True) repo = Repository() for package in local_repo.packages + locked_repository.packages: if not repo.has_package(package): repo.add_package(package) pool.add_repository(repo) # We whitelist all packages to be sure # that the latest ones are picked up whitelist = [] for pkg in locked_repository.packages: whitelist.append(pkg.name) solver = Solver( self._package, pool, self._installed_repository, locked_repository, NullIO(), ) ops = solver.solve(use_latest=whitelist) # We need to filter operations so that packages # not compatible with the current system, # or optional and not requested, are dropped self._filter_operations(ops, local_repo) self._io.new_line() # Execute operations actual_ops = [op for op in ops if not op.skipped] if not actual_ops and (self._execute_operations or self._dry_run): self._io.writeln("Nothing to install or update") if actual_ops and (self._execute_operations or self._dry_run): installs = [] updates = [] uninstalls = [] skipped = [] for op in ops: if op.skipped: skipped.append(op) continue if op.job_type == "install": installs.append( "{}:{}".format( op.package.pretty_name, op.package.full_pretty_version ) ) elif op.job_type == "update": updates.append( "{}:{}".format( op.target_package.pretty_name, op.target_package.full_pretty_version, ) ) elif op.job_type == "uninstall": uninstalls.append(op.package.pretty_name) self._io.new_line() self._io.writeln( "Package operations: " "<info>{}</> install{}, " "<info>{}</> update{}, " "<info>{}</> removal{}" "{}".format( len(installs), "" if len(installs) == 1 else "s", len(updates), "" if len(updates) == 1 else "s", len(uninstalls), "" if len(uninstalls) == 1 else "s", ", <info>{}</> skipped".format(len(skipped)) if skipped and self.is_verbose() else "", ) ) # Writing lock before installing if self._update and self._write_lock: updated_lock = self._locker.set_lock_data( self._package, local_repo.packages ) if updated_lock: self._io.writeln("") self._io.writeln("<info>Writing lock file</>") self._io.writeln("") for op in ops: self._execute(op)
def _do_install(self, local_repo): from poetry.puzzle import Solver locked_repository = Repository() if self._update: if self._locker.is_locked() and not self._lock: locked_repository = self._locker.locked_repository(True) # If no packages have been whitelisted (The ones we want to update), # we whitelist every package in the lock file. if not self._whitelist: for pkg in locked_repository.packages: self._whitelist.append(pkg.name) # Checking extras for extra in self._extras: if extra not in self._package.extras: raise ValueError( "Extra [{}] is not specified.".format(extra)) self._io.write_line("<info>Updating dependencies</>") solver = Solver( self._package, self._pool, self._installed_repository, locked_repository, self._io, remove_untracked=self._remove_untracked, ) ops = solver.solve(use_latest=self._whitelist) else: self._io.write_line( "<info>Installing dependencies from lock file</>") locked_repository = self._locker.locked_repository(True) if not self._locker.is_fresh(): self._io.write_line( "<warning>" "Warning: The lock file is not up to date with " "the latest changes in pyproject.toml. " "You may be getting outdated dependencies. " "Run update to update them." "</warning>") for extra in self._extras: if extra not in self._locker.lock_data.get("extras", {}): raise ValueError( "Extra [{}] is not specified.".format(extra)) # If we are installing from lock # Filter the operations by comparing it with what is # currently installed ops = self._get_operations_from_lock(locked_repository) self._populate_local_repo(local_repo, ops) if self._update: self._write_lock_file(local_repo) if self._lock: # If we are only in lock mode, no need to go any further return 0 root = self._package if not self.is_dev_mode(): root = root.clone() del root.dev_requires[:] elif self.is_dev_only(): root = root.clone() del root.requires[:] if self._io.is_verbose(): self._io.write_line("") self._io.write_line( "<info>Finding the necessary packages for the current system</>" ) # We resolve again by only using the lock file pool = Pool(ignore_repository_names=True) # Making a new repo containing the packages # newly resolved and the ones from the current lock file repo = Repository() for package in local_repo.packages + locked_repository.packages: if not repo.has_package(package): repo.add_package(package) pool.add_repository(repo) # We whitelist all packages to be sure # that the latest ones are picked up whitelist = [] for pkg in locked_repository.packages: whitelist.append(pkg.name) solver = Solver( root, pool, self._installed_repository, locked_repository, NullIO(), remove_untracked=self._remove_untracked, ) with solver.use_environment(self._env): ops = solver.solve(use_latest=whitelist) # We need to filter operations so that packages # not compatible with the current system, # or optional and not requested, are dropped self._filter_operations(ops, local_repo) # Execute operations return self._execute(ops)
def _do_install(self, local_repo): locked_repository = Repository() if self._update: if self._locker.is_locked() and not self._lock: locked_repository = self._locker.locked_repository(True) # If no packages have been whitelisted (The ones we want to update), # we whitelist every package in the lock file. if not self._whitelist: for pkg in locked_repository.packages: self._whitelist.append(pkg.name) # Checking extras for extra in self._extras: if extra not in self._package.extras: raise ValueError( "Extra [{}] is not specified.".format(extra)) self._io.writeln("<info>Updating dependencies</>") solver = Solver( self._package, self._pool, self._installed_repository, locked_repository, self._io, ) ops = solver.solve(use_latest=self._whitelist) else: self._io.writeln("<info>Installing dependencies from lock file</>") locked_repository = self._locker.locked_repository(True) if not self._locker.is_fresh(): self._io.writeln( "<warning>" "Warning: The lock file is not up to date with " "the latest changes in pyproject.toml. " "You may be getting outdated dependencies. " "Run update to update them." "</warning>") for extra in self._extras: if extra not in self._locker.lock_data.get("extras", {}): raise ValueError( "Extra [{}] is not specified.".format(extra)) # If we are installing from lock # Filter the operations by comparing it with what is # currently installed ops = self._get_operations_from_lock(locked_repository) self._populate_local_repo(local_repo, ops) if self._update: self._write_lock_file(local_repo) if self._lock: # If we are only in lock mode, no need to go any further return 0 root = self._package if not self.is_dev_mode(): root = root.clone() del root.dev_requires[:] with root.with_python_versions(".".join( [str(i) for i in self._env.version_info[:3]])): # We resolve again by only using the lock file pool = Pool() # Making a new repo containing the packages # newly resolved and the ones from the current lock file locked_repository = self._locker.locked_repository(True) repo = Repository() for package in local_repo.packages + locked_repository.packages: if not repo.has_package(package): repo.add_package(package) pool.add_repository(repo) # We whitelist all packages to be sure # that the latest ones are picked up whitelist = [] for pkg in locked_repository.packages: whitelist.append(pkg.name) solver = Solver(root, pool, self._installed_repository, locked_repository, NullIO()) ops = solver.solve(use_latest=whitelist) # We need to filter operations so that packages # not compatible with the current system, # or optional and not requested, are dropped self._filter_operations(ops, local_repo) self._io.new_line() # Execute operations actual_ops = [op for op in ops if not op.skipped] if not actual_ops and (self._execute_operations or self._dry_run): self._io.writeln("Nothing to install or update") if actual_ops and (self._execute_operations or self._dry_run): installs = [] updates = [] uninstalls = [] skipped = [] for op in ops: if op.skipped: skipped.append(op) continue if op.job_type == "install": installs.append("{}:{}".format( op.package.pretty_name, op.package.full_pretty_version)) elif op.job_type == "update": updates.append("{}:{}".format( op.target_package.pretty_name, op.target_package.full_pretty_version, )) elif op.job_type == "uninstall": uninstalls.append(op.package.pretty_name) self._io.new_line() self._io.writeln("Package operations: " "<info>{}</> install{}, " "<info>{}</> update{}, " "<info>{}</> removal{}" "{}".format( len(installs), "" if len(installs) == 1 else "s", len(updates), "" if len(updates) == 1 else "s", len(uninstalls), "" if len(uninstalls) == 1 else "s", ", <info>{}</> skipped".format(len(skipped)) if skipped and self.is_verbose() else "", )) self._io.writeln("") for op in ops: self._execute(op)
def test_pool_raises_package_not_found_when_no_package_is_found(): pool = Pool() pool.add_repository(Repository()) with pytest.raises(PackageNotFound): pool.package("foo", "1.0.0")
class InitCommand(Command): """ Creates a basic <comment>pyproject.toml</> file in the current directory. init {--name= : Name of the package} {--description= : Description of the package} {--author= : Author name of the package} {--dependency=* : Package to require with an optional version constraint, e.g. requests:^2.10.0 or requests=2.11.1} {--dev-dependency=* : Package to require for development with an optional version constraint, e.g. requests:^2.10.0 or requests=2.11.1} {--l|license= : License of the package} """ help = """\ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in the current directory. """ def __init__(self): super(InitCommand, self).__init__() self._pool = None def handle(self): from poetry.layouts import layout from poetry.utils._compat import Path from poetry.vcs.git import GitConfig if (Path.cwd() / "pyproject.toml").exists(): self.error("A pyproject.toml file already exists.") return 1 vcs_config = GitConfig() self.line( [ "", "This command will guide you through creating your <info>pyproject.toml</> config.", "", ] ) name = self.option("name") if not name: name = Path.cwd().name.lower() question = self.create_question( "Package name [<comment>{}</comment>]: ".format(name), default=name ) name = self.ask(question) version = "0.1.0" question = self.create_question( "Version [<comment>{}</comment>]: ".format(version), default=version ) version = self.ask(question) description = self.option("description") or "" question = self.create_question( "Description [<comment>{}</comment>]: ".format(description), default=description, ) description = self.ask(question) author = self.option("author") if not author and vcs_config and vcs_config.get("user.name"): author = vcs_config["user.name"] author_email = vcs_config.get("user.email") if author_email: author += " <{}>".format(author_email) question = self.create_question( "Author [<comment>{}</comment>, n to skip]: ".format(author), default=author ) question.validator = lambda v: self._validate_author(v, author) author = self.ask(question) if not author: authors = [] else: authors = [author] license = self.option("license") or "" question = self.create_question( "License [<comment>{}</comment>]: ".format(license), default=license ) question.validator = self._validate_license license = self.ask(question) question = self.create_question("Compatible Python versions [*]: ", default="*") python = self.ask(question) self.line("") requirements = {} question = "Would you like to define your dependencies" " (require) interactively?" if self.confirm(question, True): requirements = self._format_requirements( self._determine_requirements(self.option("dependency")) ) dev_requirements = {} question = "Would you like to define your dev dependencies" " (require-dev) interactively" if self.confirm(question, True): dev_requirements = self._format_requirements( self._determine_requirements(self.option("dev-dependency")) ) layout_ = layout("standard")( name, version, description=description, author=authors[0] if authors else None, license=license, python=python, dependencies=requirements, dev_dependencies=dev_requirements, ) content = layout_.generate_poetry_content() if self.input.is_interactive(): self.line("<info>Generated file</info>") self.line(["", content, ""]) if not self.confirm("Do you confirm generation?", True): self.line("<error>Command aborted</error>") return 1 with (Path.cwd() / "pyproject.toml").open("w") as f: f.write(content) def _determine_requirements( self, requires, allow_prereleases=False # type: List[str] # type: bool ): # type: (...) -> List[str] if not requires: requires = [] package = self.ask("Search for package:") while package is not None: matches = self._get_pool().search(package) if not matches: self.line("<error>Unable to find package</error>") package = False else: choices = [] for found_package in matches: choices.append(found_package.pretty_name) self.line( "Found <info>{}</info> packages matching <info>{}</info>".format( len(matches), package ) ) package = self.choice( "\nEnter package # to add, or the complete package name if it is not listed", choices, attempts=3, ) # no constraint yet, determine the best version automatically if package is not False and " " not in package: question = self.create_question( "Enter the version constraint to require " "(or leave blank to use the latest version):" ) question.attempts = 3 question.validator = lambda x: (x or "").strip() or False constraint = self.ask(question) if constraint is False: _, constraint = self._find_best_version_for_package(package) self.line( "Using version <info>{}</info> for <info>{}</info>".format( constraint, package ) ) package += " {}".format(constraint) if package is not False: requires.append(package) package = self.ask("\nSearch for a package:") return requires requires = self._parse_name_version_pairs(requires) result = [] for requirement in requires: if "version" not in requirement: # determine the best version automatically name, version = self._find_best_version_for_package( requirement["name"], allow_prereleases=allow_prereleases ) requirement["version"] = version requirement["name"] = name self.line( "Using version <info>{}</> for <info>{}</>".format(version, name) ) else: # check that the specified version/constraint exists # before we proceed name, _ = self._find_best_version_for_package( requirement["name"], requirement["version"], allow_prereleases=allow_prereleases, ) requirement["name"] = name result.append("{} {}".format(requirement["name"], requirement["version"])) return result def _find_best_version_for_package( self, name, required_version=None, allow_prereleases=False ): # type: (...) -> Tuple[str, str] from poetry.version.version_selector import VersionSelector selector = VersionSelector(self._get_pool()) package = selector.find_best_candidate( name, required_version, allow_prereleases=allow_prereleases ) if not package: # TODO: find similar raise ValueError( "Could not find a matching version of package {}".format(name) ) return (package.pretty_name, selector.find_recommended_require_version(package)) def _parse_name_version_pairs(self, pairs): # type: (list) -> list result = [] for i in range(len(pairs)): pair = re.sub("^([^=: ]+)[=: ](.*)$", "\\1 \\2", pairs[i].strip()) pair = pair.strip() if " " in pair: name, version = pair.split(" ", 2) result.append({"name": name, "version": version}) else: result.append({"name": pair}) return result def _format_requirements(self, requirements): # type: (List[str]) -> dict requires = {} requirements = self._parse_name_version_pairs(requirements) for requirement in requirements: requires[requirement["name"]] = requirement["version"] return requires def _validate_author(self, author, default): from poetry.packages.package import AUTHOR_REGEX author = author or default if author in ["n", "no"]: return m = AUTHOR_REGEX.match(author) if not m: raise ValueError( "Invalid author string. Must be in the format: " "John Smith <*****@*****.**>" ) return author def _validate_license(self, license): from poetry.spdx import license_by_id if license: license_by_id(license) return license def _get_pool(self): from poetry.repositories import Pool from poetry.repositories.pypi_repository import PyPiRepository if isinstance(self, VenvCommand): return self.poetry.pool if self._pool is None: self._pool = Pool() self._pool.add_repository(PyPiRepository()) return self._pool
class InitCommand(Command): name = "init" description = ( "Creates a basic <comment>pyproject.toml</> file in the current directory." ) options = [ option("name", None, "Name of the package.", flag=False), option("description", None, "Description of the package.", flag=False), option("author", None, "Author name of the package.", flag=False), option("python", None, "Compatible Python versions.", flag=False), option( "dependency", None, "Package to require, with an optional version constraint, " "e.g. requests:^2.10.0 or requests=2.11.1.", flag=False, multiple=True, ), option( "dev-dependency", None, "Package to require for development, with an optional version constraint, " "e.g. requests:^2.10.0 or requests=2.11.1.", flag=False, multiple=True, ), option("license", "l", "License of the package.", flag=False), ] help = """\ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the current directory. """ def __init__(self) -> None: super(InitCommand, self).__init__() self._pool = None def handle(self) -> int: from pathlib import Path from poetry.core.vcs.git import GitConfig from poetry.layouts import layout from poetry.utils.env import SystemEnv pyproject = PyProjectTOML(Path.cwd() / "pyproject.toml") if pyproject.file.exists(): if pyproject.is_poetry_project(): self.line( "<error>A pyproject.toml file with a poetry section already exists.</error>" ) return 1 if pyproject.data.get("build-system"): self.line( "<error>A pyproject.toml file with a defined build-system already exists.</error>" ) return 1 vcs_config = GitConfig() self.line("") self.line( "This command will guide you through creating your <info>pyproject.toml</> config." ) self.line("") name = self.option("name") if not name: name = Path.cwd().name.lower() question = self.create_question( "Package name [<comment>{}</comment>]: ".format(name), default=name ) name = self.ask(question) version = "0.1.0" question = self.create_question( "Version [<comment>{}</comment>]: ".format(version), default=version ) version = self.ask(question) description = self.option("description") or "" question = self.create_question( "Description [<comment>{}</comment>]: ".format(description), default=description, ) description = self.ask(question) author = self.option("author") if not author and vcs_config and vcs_config.get("user.name"): author = vcs_config["user.name"] author_email = vcs_config.get("user.email") if author_email: author += " <{}>".format(author_email) question = self.create_question( "Author [<comment>{}</comment>, n to skip]: ".format(author), default=author ) question.set_validator(lambda v: self._validate_author(v, author)) author = self.ask(question) if not author: authors = [] else: authors = [author] license = self.option("license") or "" question = self.create_question( "License [<comment>{}</comment>]: ".format(license), default=license ) question.set_validator(self._validate_license) license = self.ask(question) python = self.option("python") if not python: current_env = SystemEnv(Path(sys.executable)) default_python = "^{}".format( ".".join(str(v) for v in current_env.version_info[:2]) ) question = self.create_question( "Compatible Python versions [<comment>{}</comment>]: ".format( default_python ), default=default_python, ) python = self.ask(question) self.line("") requirements = {} if self.option("dependency"): requirements = self._format_requirements( self._determine_requirements(self.option("dependency")) ) question = "Would you like to define your main dependencies interactively?" help_message = ( "You can specify a package in the following forms:\n" " - A single name (<b>requests</b>)\n" " - A name and a constraint (<b>requests@^2.23.0</b>)\n" " - A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)\n" " - A git url with a revision (<b>git+https://github.com/python-poetry/poetry.git#develop</b>)\n" " - A file path (<b>../my-package/my-package.whl</b>)\n" " - A directory (<b>../my-package/</b>)\n" " - A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\n" ) help_displayed = False if self.confirm(question, True): self.line(help_message) help_displayed = True requirements.update( self._format_requirements(self._determine_requirements([])) ) self.line("") dev_requirements = {} if self.option("dev-dependency"): dev_requirements = self._format_requirements( self._determine_requirements(self.option("dev-dependency")) ) question = ( "Would you like to define your development dependencies interactively?" ) if self.confirm(question, True): if not help_displayed: self.line(help_message) dev_requirements.update( self._format_requirements(self._determine_requirements([])) ) self.line("") layout_ = layout("standard")( name, version, description=description, author=authors[0] if authors else None, license=license, python=python, dependencies=requirements, dev_dependencies=dev_requirements, ) content = layout_.generate_poetry_content(original=pyproject) if self.io.is_interactive(): self.line("<info>Generated file</info>") self.line("") self.line(content) self.line("") if not self.confirm("Do you confirm generation?", True): self.line("<error>Command aborted</error>") return 1 with (Path.cwd() / "pyproject.toml").open("w", encoding="utf-8") as f: f.write(content) def _determine_requirements( self, requires: List[str], allow_prereleases: bool = False, source: Optional[str] = None, ) -> List[Dict[str, Union[str, List[str]]]]: if not requires: requires = [] package = self.ask( "Search for package to add (or leave blank to continue):" ) while package is not None: constraint = self._parse_requirements([package])[0] if ( "git" in constraint or "url" in constraint or "path" in constraint or "version" in constraint ): self.line("Adding <info>{}</info>".format(package)) requires.append(constraint) package = self.ask("\nAdd a package:") continue matches = self._get_pool().search(constraint["name"]) if not matches: self.line("<error>Unable to find package</error>") package = False else: choices = [] matches_names = [p.name for p in matches] exact_match = constraint["name"] in matches_names if exact_match: choices.append( matches[matches_names.index(constraint["name"])].pretty_name ) for found_package in matches: if len(choices) >= 10: break if found_package.name.lower() == constraint["name"].lower(): continue choices.append(found_package.pretty_name) self.line( "Found <info>{}</info> packages matching <c1>{}</c1>".format( len(matches), package ) ) package = self.choice( "\nEnter package # to add, or the complete package name if it is not listed", choices, attempts=3, ) # package selected by user, set constraint name to package name if package is not False: constraint["name"] = package # no constraint yet, determine the best version automatically if package is not False and "version" not in constraint: question = self.create_question( "Enter the version constraint to require " "(or leave blank to use the latest version):" ) question.attempts = 3 question.validator = lambda x: (x or "").strip() or False package_constraint = self.ask(question) if package_constraint is None: _, package_constraint = self._find_best_version_for_package( package ) self.line( "Using version <b>{}</b> for <c1>{}</c1>".format( package_constraint, package ) ) constraint["version"] = package_constraint if package is not False: requires.append(constraint) package = self.ask("\nAdd a package:") return requires requires = self._parse_requirements(requires) result = [] for requirement in requires: if "git" in requirement or "url" in requirement or "path" in requirement: result.append(requirement) continue elif "version" not in requirement: # determine the best version automatically name, version = self._find_best_version_for_package( requirement["name"], allow_prereleases=allow_prereleases, source=source, ) requirement["version"] = version requirement["name"] = name self.line( "Using version <b>{}</b> for <c1>{}</c1>".format(version, name) ) else: # check that the specified version/constraint exists # before we proceed name, _ = self._find_best_version_for_package( requirement["name"], requirement["version"], allow_prereleases=allow_prereleases, source=source, ) requirement["name"] = name result.append(requirement) return result def _find_best_version_for_package( self, name: str, required_version: Optional[str] = None, allow_prereleases: bool = False, source: Optional[str] = None, ) -> Tuple[str, str]: from poetry.version.version_selector import VersionSelector selector = VersionSelector(self._get_pool()) package = selector.find_best_candidate( name, required_version, allow_prereleases=allow_prereleases, source=source ) if not package: # TODO: find similar raise ValueError( "Could not find a matching version of package {}".format(name) ) return package.pretty_name, selector.find_recommended_require_version(package) def _parse_requirements(self, requirements: List[str]) -> List[Dict[str, str]]: from poetry.puzzle.provider import Provider result = [] try: cwd = self.poetry.file.parent except (PyProjectException, RuntimeError): cwd = Path.cwd() for requirement in requirements: requirement = requirement.strip() extras = [] extras_m = re.search(r"\[([\w\d,-_ ]+)\]$", requirement) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] requirement, _ = requirement.split("[") url_parsed = urllib.parse.urlparse(requirement) if url_parsed.scheme and url_parsed.netloc: # Url if url_parsed.scheme in ["git+https", "git+ssh"]: from poetry.core.vcs.git import Git from poetry.core.vcs.git import ParsedUrl parsed = ParsedUrl.parse(requirement) url = Git.normalize_url(requirement) pair = dict([("name", parsed.name), ("git", url.url)]) if parsed.rev: pair["rev"] = url.revision if extras: pair["extras"] = extras package = Provider.get_package_from_vcs( "git", url.url, rev=pair.get("rev") ) pair["name"] = package.name result.append(pair) continue elif url_parsed.scheme in ["http", "https"]: package = Provider.get_package_from_url(requirement) pair = dict([("name", package.name), ("url", package.source_url)]) if extras: pair["extras"] = extras result.append(pair) continue elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath( requirement ).exists(): path = cwd.joinpath(requirement) if path.is_file(): package = Provider.get_package_from_file(path.resolve()) else: package = Provider.get_package_from_directory(path) result.append( dict( [ ("name", package.name), ("path", path.relative_to(cwd).as_posix()), ] + ([("extras", extras)] if extras else []) ) ) continue pair = re.sub( "^([^@=: ]+)(?:@|==|(?<![<>~!])=|:| )(.*)$", "\\1 \\2", requirement ) pair = pair.strip() require = dict() if " " in pair: name, version = pair.split(" ", 2) extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] name, _ = name.split("[") require["name"] = name if version != "latest": require["version"] = version else: m = re.match( r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip() ) if m: name, constraint = m.group(1), m.group(2) extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] name, _ = name.split("[") require["name"] = name require["version"] = constraint else: extras_m = re.search(r"\[([\w\d,-_]+)\]$", pair) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] pair, _ = pair.split("[") require["name"] = pair if extras: require["extras"] = extras result.append(require) return result def _format_requirements( self, requirements: List[Dict[str, str]] ) -> Dict[str, Union[str, Dict[str, str]]]: requires = {} for requirement in requirements: name = requirement.pop("name") if "version" in requirement and len(requirement) == 1: constraint = requirement["version"] else: constraint = inline_table() constraint.trivia.trail = "\n" constraint.update(requirement) requires[name] = constraint return requires def _validate_author(self, author: str, default: str) -> Optional[str]: from poetry.core.packages.package import AUTHOR_REGEX author = author or default if author in ["n", "no"]: return m = AUTHOR_REGEX.match(author) if not m: raise ValueError( "Invalid author string. Must be in the format: " "John Smith <*****@*****.**>" ) return author def _validate_license(self, license: str) -> str: from poetry.core.spdx import license_by_id if license: license_by_id(license) return license def _get_pool(self) -> "Pool": from poetry.repositories import Pool from poetry.repositories.pypi_repository import PyPiRepository if isinstance(self, EnvCommand): return self.poetry.pool if self._pool is None: self._pool = Pool() self._pool.add_repository(PyPiRepository()) return self._pool
class InitCommand(Command): """ Creates a basic <comment>pyproject.toml</> file in the current directory. init {--name= : Name of the package} {--description= : Description of the package} {--author= : Author name of the package} {--dependency=* : Package to require with an optional version constraint, e.g. requests:^2.10.0 or requests=2.11.1} {--dev-dependency=* : Package to require for development with an optional version constraint, e.g. requests:^2.10.0 or requests=2.11.1} {--l|license= : License of the package} """ help = """\ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in the current directory. """ def __init__(self): super(InitCommand, self).__init__() self._pool = None def handle(self): from poetry.layouts import layout from poetry.utils._compat import Path from poetry.vcs.git import GitConfig if (Path.cwd() / 'pyproject.toml').exists(): self.error('A pyproject.toml file already exists.') return 1 vcs_config = GitConfig() self.line([ '', 'This command will guide you through creating your <info>poetry.toml</> config.', '' ]) name = self.option('name') if not name: name = Path.cwd().name.lower() question = self.create_question( 'Package name [<comment>{}</comment>]: '.format(name), default=name ) name = self.ask(question) version = '0.1.0' question = self.create_question( 'Version [<comment>{}</comment>]: '.format(version), default=version ) version = self.ask(question) description = self.option('description') or '' question = self.create_question( 'Description [<comment>{}</comment>]: '.format(description), default=description ) description = self.ask(question) author = self.option('author') if not author and vcs_config and vcs_config.get('user.name'): author = vcs_config['user.name'] author_email = vcs_config.get('user.email') if author_email: author += ' <{}>'.format(author_email) question = self.create_question( 'Author [<comment>{}</comment>, n to skip]: '.format(author), default=author ) question.validator = lambda v: self._validate_author(v, author) author = self.ask(question) if not author: authors = [] else: authors = [author] license = self.option('license') or '' question = self.create_question( 'License [<comment>{}</comment>]: '.format(license), default=license ) license = self.ask(question) question = self.create_question( 'Compatible Python versions [*]: ', default='*' ) python = self.ask(question) self.line('') requirements = [] question = 'Would you like to define your dependencies' \ ' (require) interactively?' if self.confirm(question, True): requirements = self._format_requirements( self._determine_requirements(self.option('dependency')) ) dev_requirements = [] question = 'Would you like to define your dev dependencies' \ ' (require-dev) interactively' if self.confirm(question, True): dev_requirements = self._format_requirements( self._determine_requirements(self.option('dev-dependency')) ) layout_ = layout('standard')( name, version, description=description, author=authors[0] if authors else None, license=license, python=python, dependencies=requirements, dev_dependencies=dev_requirements ) content = layout_.generate_poetry_content() if self.input.is_interactive(): self.line('<info>Generated file</info>') self.line(['', content, '']) if not self.confirm('Do you confirm generation?', True): self.line('<error>Command aborted</error>') return 1 with (Path.cwd() / 'pyproject.toml').open('w') as f: f.write(content) def _determine_requirements(self, requires, # type: List[str] allow_prereleases=False, # type: bool ): # type: (...) -> List[str] if not requires: requires = [] package = self.ask('Search for package:') while package is not None: matches = self._get_pool().search(package) if not matches: self.line('<error>Unable to find package</error>') package = False else: choices = [] for found_package in matches: choices.append(found_package.pretty_name) self.line( 'Found <info>{}</info> packages matching <info>{}</info>' .format( len(matches), package ) ) package = self.choice( '\nEnter package # to add, or the complete package name if it is not listed', choices, attempts=3 ) # no constraint yet, determine the best version automatically if package is not False and ' ' not in package: question = self.create_question( 'Enter the version constraint to require ' '(or leave blank to use the latest version):' ) question.attempts = 3 question.validator = lambda x: (x or '').strip() or False constraint = self.ask(question) if constraint is False: _, constraint = self._find_best_version_for_package(package) self.line( 'Using version <info>{}</info> for <info>{}</info>' .format(constraint, package) ) package += ' {}'.format(constraint) if package is not False: requires.append(package) package = self.ask('\nSearch for a package:') return requires requires = self._parse_name_version_pairs(requires) result = [] for requirement in requires: if 'version' not in requirement: # determine the best version automatically name, version = self._find_best_version_for_package( requirement['name'], allow_prereleases=allow_prereleases ) requirement['version'] = version requirement['name'] = name self.line( 'Using version <info>{}</> for <info>{}</>' .format(version, name) ) else: # check that the specified version/constraint exists # before we proceed name, _ = self._find_best_version_for_package( requirement['name'], requirement['version'], allow_prereleases=allow_prereleases ) requirement['name'] = name result.append( '{} {}'.format(requirement['name'], requirement['version']) ) return result def _find_best_version_for_package(self, name, required_version=None, allow_prereleases=False ): # type: (...) -> Tuple[str, str] from poetry.version.version_selector import VersionSelector selector = VersionSelector(self._get_pool()) package = selector.find_best_candidate( name, required_version, allow_prereleases=allow_prereleases ) if not package: # TODO: find similar raise ValueError( 'Could not find a matching version of package {}'.format(name) ) return ( package.pretty_name, selector.find_recommended_require_version(package) ) def _parse_name_version_pairs(self, pairs): # type: (list) -> list result = [] for i in range(len(pairs)): pair = re.sub('^([^=: ]+)[=: ](.*)$', '\\1 \\2', pairs[i].strip()) pair = pair.strip() if ' ' in pair: name, version = pair.split(' ', 2) result.append({ 'name': name, 'version': version }) else: result.append({ 'name': pair }) return result def _format_requirements(self, requirements): # type: (List[str]) -> dict requires = {} requirements = self._parse_name_version_pairs(requirements) for requirement in requirements: requires[requirement['name']] = requirement['version'] return requires def _validate_author(self, author, default): from poetry.packages.package import AUTHOR_REGEX author = author or default if author in ['n', 'no']: return m = AUTHOR_REGEX.match(author) if not m: raise ValueError( 'Invalid author string. Must be in the format: ' 'John Smith <*****@*****.**>' ) return author def _get_pool(self): from poetry.repositories import Pool from poetry.repositories.pypi_repository import PyPiRepository if isinstance(self, VenvCommand): return self.poetry.pool if self._pool is None: self._pool = Pool() self._pool.add_repository(PyPiRepository()) return self._pool