def test_option(self): validator = Integer() opt = option( 'foo', 'f', 'The foo option.', flag=True, validator=validator ) self.assertIsInstance(opt, InputOption) self.assertEqual('foo', opt.get_name()) self.assertEqual('The foo option.', opt.get_description()) self.assertTrue(opt.is_flag()) self.assertEqual(validator, opt.get_validator()) opt = option( 'foo', 'f', 'The foo option.', value_required=True, is_list=True, default=['default'], validator=validator ) self.assertIsInstance(opt, InputOption) self.assertEqual('foo', opt.get_name()) self.assertEqual('The foo option.', opt.get_description()) self.assertFalse(opt.is_flag()) self.assertTrue(opt.is_value_required()) self.assertTrue(opt.is_list()) self.assertEqual(validator, opt.get_validator())
class DiscoverCommand(Command): name = "discover" description = "Test dsicovery functionality" arguments = [ argument("parameter", description="What to discover", optional=False) ] options = [ option("input-glob", flag=False), option("output-glob", flag=False) ] def handle(self) -> None: parameter = self.argument("parameter") if parameter == "tests": input_glob = self.option("input-glob") output_glob = self.option("output-glob") if not input_glob: self.line("<error>input-glob option must be specified</error>") return if not output_glob: self.line( "<error>output-glob option must be specified</error>") return tests = Discovery.find_test_cases(input_glob, output_glob) self.line(f"len of tests: {len(tests)}") for t in tests: print(f"key: {t}; input: {tests[t][0]}; output: {tests[t][1]}")
class PublishCommand(Command): name = "publish" description = "Publishes a package to a remote repository." options = [ option("repository", "r", "The repository to publish the package to.", flag=False), option("username", "u", "The username to access the repository.", flag=False), option("password", "p", "The password to access the repository.", flag=False), option("build", None, "Build the package before publishing."), ] help = """The publish command builds and uploads the package to a remote repository. By default, it will upload to PyPI but if you pass the --repository option it will upload to it instead. The --repository option should match the name of a configured repository using the config command. """ def handle(self): from poetry.masonry.publishing.publisher import Publisher publisher = Publisher(self.poetry, self.io) # Building package first, if told if self.option("build"): if publisher.files: if not self.confirm( "There are <info>{}</info> files ready for publishing. " "Build anyway?".format(len(publisher.files))): self.line_error("<error>Aborted!</error>") return 1 self.call("build") files = publisher.files if not files: self.line_error( "<error>No files to publish. " "Run poetry build first or use the --build option.</error>") return 1 self.line("") publisher.publish(self.option("repository"), self.option("username"), self.option("password"))
class TrainSentencePieceModelCommand(BaseCommand): name = "train-spm" description = "Train Sentence Piece Model for BPE encoding." arguments = [argument("config", description="Config to use for SPM training.")] options = [ option( "serialization-dir", "s", description="Directory to save trained SPM Model.", flag=False, value_required=False, ), option( "extra-vars", None, description=( "Extra variables to inject in JsonNet config in such format: " "{key_name1}={new_value1},{key_name2}={new_value2},..." ), flag=False, value_required=False, ), ] def handle(self) -> None: extra_vars = self.parse_extra_vars() config = Params.from_file(self.argument("config"), ext_vars=extra_vars) # Add serialization directory to config and create it serialization_dir = Path(self.option("serialization-dir")) self.prepare_directory(serialization_dir) # Log config to console and save config["model_prefix"] = str(serialization_dir / config["model_prefix"]) logger.info( "Config: {}".format(json.dumps(config.as_flat_dict(), indent=2, ensure_ascii=False)) ) with (serialization_dir / "config.json").open("w", encoding="utf-8") as file: json.dump(config.as_dict(quiet=True), file, indent=2, ensure_ascii=False) # Train SPM Model spm.SentencePieceTrainer.train(**config) self.line( f"Finished SentencePiece model training and saved at path: `{serialization_dir}`", style="info", ) def parse_extra_vars(self) -> Dict[str, str]: extra_vars = self.option("extra-vars") regex = r"([a-z0-9\_\-\.\+\\\/]+)=([a-z0-9\_\-\.\+\\\/]+)" return ( {param: value for param, value in re.findall(regex, extra_vars, flags=re.I)} if extra_vars is not None else None )
class BuildCommand(EnvCommand): name = "build" description = "Builds a package, as a tarball and a wheel by default." options = [ option("format", "f", "Limit the format to either wheel or sdist.", flag=False) ] def handle(self): from poetry.masonry import Builder fmt = "all" if self.option("format"): fmt = self.option("format") package = self.poetry.package self.line("Building <info>{}</> (<comment>{}</>)".format( package.pretty_name, package.version)) builder = Builder(self.poetry, self.env, self.io) builder.build(fmt)
class EnvListCommand(Command): name = "list" description = "Lists all virtualenvs associated with the current project." options = [ option("full-path", None, "Output the full paths of the virtualenvs.") ] def handle(self): from poetry.utils.env import EnvManager poetry = self.poetry manager = EnvManager(poetry.config) current_env = manager.get(self.poetry.file.parent) for venv in manager.list(self.poetry.file.parent): name = venv.path.name if self.option("full-path"): name = str(venv.path) if venv == current_env: self.line("<info>{} (Activated)</info>".format(name)) continue self.line(name)
class LockCommand(InstallerCommand): name = "lock" description = "Locks the project dependencies." options = [ option("no-update", None, "Do not update locked versions, only refresh lock file."), ] help = """ The <info>lock</info> command reads the <comment>pyproject.toml</> file from the current directory, processes it, and locks the dependencies in the <comment>poetry.lock</> file. <info>poetry lock</info> """ loggers = ["poetry.repositories.pypi_repository"] def handle(self): self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False)) self._installer.lock(update=not self.option("no-update")) return self._installer.run()
class BuildCommand(EnvCommand): name = "build" description = "Builds a package, as a tarball and a wheel by default." options = [ option("format", "f", "Limit the format to either sdist or wheel.", flag=False) ] loggers = [ "poetry.core.masonry.builders.sdist", "poetry.core.masonry.builders.wheel", ] def handle(self): from poetry.core.masonry import Builder fmt = "all" if self.option("format"): fmt = self.option("format") package = self.poetry.package self.line("Building <c1>{}</c1> (<c2>{}</c2>)".format( package.pretty_name, package.version)) builder = Builder(self.poetry) builder.build(fmt)
class SearchCommand(Command): name = "search" description = "Searches for packages on remote repositories." arguments = [ argument("tokens", "The tokens to search for.", multiple=True) ] options = [option("only-name", "N", "Search only by name.")] def handle(self): from poetry.repositories.pypi_repository import PyPiRepository flags = PyPiRepository.SEARCH_FULLTEXT if self.option("only-name"): flags = PyPiRepository.SEARCH_NAME results = PyPiRepository().search(self.argument("tokens"), flags) for result in results: self.line("") name = "<info>{}</>".format(result.name) name += " (<comment>{}</>)".format(result.version) self.line(name) if result.description: self.line(" {}".format(result.description))
class ExportCommand(Command): name = "export" description = "Exports the lock file to alternative formats." options = [ option("format", "f", "Format to export to.", flag=False), option("without-hashes", None, "Exclude hashes from the exported file."), option("dev", None, "Include development dependencies."), ] def handle(self): fmt = self.option("format") if fmt not in Exporter.ACCEPTED_FORMATS: raise ValueError("Invalid export format: {}".format(fmt)) locker = self.poetry.locker if not locker.is_locked(): self.line( "<comment>The lock file does not exist. Locking.</comment>") options = [] if self.io.is_debug(): options.append(("-vvv", None)) elif self.io.is_very_verbose(): options.append(("-vv", None)) elif self.io.is_verbose(): options.append(("-v", None)) self.call("lock", options) if not locker.is_fresh(): self.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>") exporter = Exporter(self.poetry.locker) exporter.export( fmt, self.poetry.file.parent, with_hashes=not self.option("without-hashes"), dev=self.option("dev"), )
class UpdateCommand(EnvCommand): name = "update" description = ( "Update dependencies as according to the <comment>pyproject.toml</> file." ) arguments = [ argument("packages", "The packages to update", optional=True, multiple=True) ] options = [ option("no-dev", None, "Do not update dev dependencies."), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] loggers = ["poetry.repositories.pypi_repository"] def handle(self): from poetry.installation import Installer packages = self.argument("packages") installer = Installer(self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool) if packages: installer.whitelist({name: "*" for name in packages}) installer.dev_mode(not self.option("no-dev")) installer.dry_run(self.option("dry-run")) installer.execute_operations(not self.option("lock")) # Force update installer.update(True) return installer.run()
class RunCommand(Command): name = "run" description = "Run fate" arguments = [ argument("path", "The directory in which to run fate or path to executable", optional=True) ] options = [ option("output-type", description= "Instruct executable to print output to file or to stdout", value_required=True), option("execution-environment", description="Which environment to use to run the solution", value_required=True), option("docker", description="Use docker to run the solution", flag=True), option("debug", description="Run solution in debug mode", flag=True), option("input-file", description="Path input file of the test case", value_required=True), option("output-file", description="Path to output file of the test case", value_required=True), option("test-case-name", description="Name of the test case to execute", value_required=True), option( "concurrent", description= "Run tests concurrently (optionally specify number of concurent executions)", value_required=False), option("memory-limit", description= "Impose a memory limit (MB) when running in docker container", value_required=True), option("time-limit", description="Impose a time limit (ms) for each execution", value_required=True) ] def handle(self) -> None: pass
class ConfigureCommand(Command): name = "configure" description = "Configure the environment" arguments = [ argument("command", description="Which command to execute: show, get, or set", optional=False), argument("parameter", description="Which parameter to set or get", optional=True) ] options = [ option( "user", description= "Save configuration file in user's home directory when setting parameter", flag=True), option( "system", description= "Save configuration file in system directory when setting parameters", flag=True) ] def handle(self) -> None: command = self.argument("command") parameter = self.argument("parameter") if command == "show": config = Configuration() config.build() table = config.as_table() self.render_table(*table) elif command == "get": if not parameter: self.line("<error>Parameter name must be specified</error>") elif command == "set": if not parameter: self.line("<error>Parameter name must be specified</error>") else: self.line(f"<error>Wrong command: {command}</error>")
class UpdateCommand(InstallerCommand): name = "update" description = ( "Update the dependencies as according to the <comment>pyproject.toml</> file." ) arguments = [ argument("packages", "The packages to update", optional=True, multiple=True) ] options = [ option("no-dev", None, "Do not update the development dependencies."), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] loggers = ["poetry.repositories.pypi_repository"] def handle(self): # type: () -> int packages = self.argument("packages") self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False)) if packages: self._installer.whitelist({name: "*" for name in packages}) self._installer.dev_mode(not self.option("no-dev")) self._installer.dry_run(self.option("dry-run")) self._installer.execute_operations(not self.option("lock")) # Force update self._installer.update(True) return self._installer.run()
class EnvInfoCommand(Command): name = "info" description = "Displays information about the current environment." options = [option("path", "p", "Only display the environment's path.")] def handle(self): from poetry.utils.env import EnvManager poetry = self.poetry env = EnvManager(poetry.config).get(cwd=poetry.file.parent) if self.option("path"): if not env.is_venv(): return 1 self.write(str(env.path)) return self._display_complete_info(env) def _display_complete_info(self, env): env_python_version = ".".join(str(s) for s in env.version_info[:3]) self.line("") self.line("<b>Virtualenv</b>") listing = [ "<info>Python</info>: <comment>{}</>".format(env_python_version), "<info>Implementation</info>: <comment>{}</>".format( env.python_implementation ), "<info>Path</info>: <comment>{}</>".format( env.path if env.is_venv() else "NA" ), ] if env.is_venv(): listing.append( "<info>Valid</info>: <{tag}>{is_valid}</{tag}>".format( tag="comment" if env.is_sane() else "error", is_valid=env.is_sane() ) ) self.line("\n".join(listing)) self.line("") self.line("<b>System</b>") self.line( "\n".join( [ "<info>Platform</info>: <comment>{}</>".format(env.platform), "<info>OS</info>: <comment>{}</>".format(env.os), "<info>Python</info>: <comment>{}</>".format(env.base), ] ) )
def test_option(): opt = option("foo", "f", "Foo") assert "Foo" == opt.description assert not opt.accepts_value() assert not opt.is_value_optional() assert not opt.is_value_required() assert not opt.is_multi_valued() assert opt.default is None opt = option("foo", "f", "Foo", flag=False) assert "Foo" == opt.description assert opt.accepts_value() assert not opt.is_value_optional() assert opt.is_value_required() assert not opt.is_multi_valued() opt = option("foo", "f", "Foo", flag=False, value_required=False) assert "Foo" == opt.description assert opt.accepts_value() assert opt.is_value_optional() assert not opt.is_value_required() assert not opt.is_multi_valued() opt = option("foo", "f", "Foo", flag=False, multiple=True) assert "Foo" == opt.description assert opt.accepts_value() assert not opt.is_value_optional() assert opt.is_value_required() assert opt.is_multi_valued() assert [] == opt.default opt = option("foo", "f", "Foo", flag=False, default="bar") assert "Foo" == opt.description assert opt.accepts_value() assert not opt.is_value_optional() assert opt.is_value_required() assert not opt.is_multi_valued() assert "bar" == opt.default
class OcelCommand(Command): name = 'ocel' description = 'ocel command' arguments = [ argument('template', 'name of template', optional=True), argument('target', 'target path', optional=True) ] options = [ option('list', 'l', 'list of templates'), option( 'update', 'u', 'update ocel from github', ) ] def handle(self): ls = self.option('list') update = self.option('update') template = self.argument('template') target = self.argument('target') ocel = Ocel() if template and (ls or update): raise CommandError('Invalid format of command') if template: ocel.run(template, target) self.line('Finished.') elif ls: template_list = ocel.template_list(True) table = self.table(['Name', 'Desc']) for name, meta in template_list.items(): table.add_row([name, meta.get('desc', '')]) table.render(self.io) elif update: ocel.update() else: raise CommandError('Invalid format of command')
class MakeShardsCommand(BaseCommand): name = "make-shards" description = "Construct shards for Distributed Training from one txt file." arguments = [argument("path", description="Path to txt file.")] options = [ option( "shards", "s", description="Number of shrads to split txt file.", flag=False, value_required=False, ), option( "seed", None, default=13, description="Random seed for to randomly assign sample to a shard.", flag=False, value_required=False, ), ] def handle(self) -> None: rng = random.Random(int(self.option("seed"))) shards = int(self.option("shards")) file_path = Path(self.argument("path")) directory = file_path.parent / file_path.stem self.prepare_directory(directory) with file_path.open("r", encoding="utf-8") as file: for line in tqdm(map(lambda x: x.strip(), file), desc="Sharding dataset"): choice = rng.randint(0, shards - 1) self.write_to_shard(line, directory / f"shard-{choice}.txt") @staticmethod def write_to_shard(line: str, shard_path: Path) -> None: with shard_path.open("a", encoding="utf-8") as file: file.write(line + "\n")
class CloneCommand(Command): name = 'clone' description = 'clone repository from github' arguments = [ argument('repository', 'name of repository that you want to clone'), argument('target', 'target directory name', optional=True), ] options = [option('url', 'u', 'echo only url, not clone')] def handle(self): clone_url = url_parsing(self.argument('repository')) if self.option('url'): self.line(clone_url) return 1 clone(clone_url, self.argument('target'))
class StartAppCommand(Command): name = "startapp" description = "Create a new flubber app" arguments = [argument("name", "The app name.")] options = [option("path", None, "The path to create the app at.", flag=False)] def handle(self) -> None: from flubber.console.commands.utils import get_flubber_path if self.option("path"): path = Path(self.option("path")) / Path(self.argument("name")) else: path = Path.cwd() / Path(self.argument("name")) name = self.argument("name") if path.exists(): if list(path.glob("*")): # Directory is not empty. Aborting. raise RuntimeError( "Destination <fg=yellow>{}</> " "exists and is not empty".format(path) ) self.copy_project_folder(src=get_flubber_path(), dst=path) self.line( "Created flubber project <info>{}</> in <fg=blue>{}</>".format( name, path.relative_to(Path.cwd()) ) ) def copy_project_folder(self, src: str, dst: str) -> None: import shutil self.line("<info>Creating project ...</>") template_project_path = Path(src) / Path("conf/app_template") dst_path = Path(dst) shutil.copytree(template_project_path, dst_path) return
class ExportCommand(Command): name = "export" description = "Exports the lock file to alternative formats." options = [ option( "format", "f", "Format to export to. Currently, only requirements.txt is supported.", flag=False, default=Exporter.FORMAT_REQUIREMENTS_TXT, ), option("output", "o", "The name of the output file.", flag=False), option("without-hashes", None, "Exclude hashes from the exported file."), option("dev", None, "Include development dependencies."), option( "extras", "E", "Extra sets of dependencies to include.", flag=False, multiple=True, ), option("with-credentials", None, "Include credentials for extra indices."), ] def handle(self): # type: () -> None fmt = self.option("format") if fmt not in Exporter.ACCEPTED_FORMATS: raise ValueError("Invalid export format: {}".format(fmt)) output = self.option("output") locker = self.poetry.locker if not locker.is_locked(): self.line( "<comment>The lock file does not exist. Locking.</comment>") options = [] if self.io.is_debug(): options.append(("-vvv", None)) elif self.io.is_very_verbose(): options.append(("-vv", None)) elif self.io.is_verbose(): options.append(("-v", None)) self.call("lock", options) if not locker.is_fresh(): self.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>") exporter = Exporter(self.poetry) exporter.export( fmt, self.poetry.file.parent, output or self.io, with_hashes=not self.option("without-hashes"), dev=self.option("dev"), extras=self.option("extras"), with_credentials=self.option("with-credentials"), )
class DebugResolveCommand(InitCommand): name = "resolve" description = "Debugs dependency resolution." arguments = [ argument("package", "The packages to resolve.", optional=True, multiple=True) ] options = [ option( "extras", "E", "Extras to activate for the dependency.", flag=False, multiple=True, ), option("python", None, "Python version(s) to use for resolution.", flag=False), option("tree", None, "Display the dependency tree."), option("install", None, "Show what would be installed for the current system."), ] loggers = ["poetry.repositories.pypi_repository"] def handle(self): from poetry.core.packages.project_package import ProjectPackage from poetry.io.null_io import NullIO from poetry.puzzle import Solver from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository from poetry.utils.env import EnvManager packages = self.argument("package") if not packages: package = self.poetry.package else: # Using current pool for determine_requirements() self._pool = self.poetry.pool package = ProjectPackage( self.poetry.package.name, self.poetry.package.version ) # Silencing output is_quiet = self.io.output.is_quiet() if not is_quiet: self.io.output.set_quiet(True) requirements = self._determine_requirements(packages) if not is_quiet: self.io.output.set_quiet(False) for constraint in requirements: name = constraint.pop("name") dep = package.add_dependency(name, constraint) extras = [] for extra in self.option("extras"): if " " in extra: extras += [e.strip() for e in extra.split(" ")] else: extras.append(extra) for ex in extras: dep.extras.append(ex) package.python_versions = self.option("python") or ( self.poetry.package.python_versions ) pool = self.poetry.pool solver = Solver(package, pool, Repository(), Repository(), self._io) ops = solver.solve() self.line("") self.line("Resolution results:") self.line("") if self.option("tree"): show_command = self.application.find("show") show_command.init_styles(self.io) packages = [op.package for op in ops] repo = Repository(packages) requires = package.requires + package.dev_requires for pkg in repo.packages: for require in requires: if pkg.name == require.name: show_command.display_package_tree(self.io, pkg, repo) break return 0 table = self.table([], style="borderless") rows = [] if self.option("install"): env = EnvManager(self.poetry).get() pool = Pool() locked_repository = Repository() for op in ops: locked_repository.add_package(op.package) pool.add_repository(locked_repository) solver = Solver(package, pool, Repository(), Repository(), NullIO()) with solver.use_environment(env): ops = solver.solve() for op in ops: if self.option("install") and op.skipped: continue pkg = op.package row = [ "<c1>{}</c1>".format(pkg.name), "<b>{}</b>".format(pkg.version), "", ] if not pkg.marker.is_any(): row[2] = str(pkg.marker) rows.append(row) table.set_rows(rows) table.render(self.io)
class ConfigCommand(Command): name = "config" description = "Manages configuration settings." arguments = [ argument("key", "Setting key.", optional=True), argument("value", "Setting value.", optional=True, multiple=True), ] options = [ option("list", None, "List configuration settings."), option("unset", None, "Unset configuration setting."), option("local", None, "Set/Get from the project's local configuration."), ] help = """This command allows you to edit the poetry config settings and repositories. To add a repository: <comment>poetry config repositories.foo https://bar.com/simple/</comment> To remove a repository (repo is a short alias for repositories): <comment>poetry config --unset repo.foo</comment>""" LIST_PROHIBITED_SETTINGS = {"http-basic", "pypi-token"} @property def unique_config_values(self): from poetry.config.config import boolean_normalizer from poetry.config.config import boolean_validator from poetry.locations import CACHE_DIR from poetry.utils._compat import Path unique_config_values = { "cache-dir": ( str, lambda val: str(Path(val)), str(Path(CACHE_DIR) / "virtualenvs"), ), "virtualenvs.create": (boolean_validator, boolean_normalizer, True), "virtualenvs.in-project": (boolean_validator, boolean_normalizer, False), "virtualenvs.path": ( str, lambda val: str(Path(val)), str(Path(CACHE_DIR) / "virtualenvs"), ), } return unique_config_values def handle(self): from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR from poetry.utils._compat import Path from poetry.utils._compat import basestring from poetry.utils.toml_file import TomlFile config = Factory.create_config(self.io) config_file = TomlFile(Path(CONFIG_DIR) / "config.toml") try: local_config_file = TomlFile(self.poetry.file.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) except RuntimeError: local_config_file = TomlFile(Path.cwd() / "poetry.toml") if self.option("local"): config.set_config_source(FileConfigSource(local_config_file)) if not config_file.exists(): config_file.path.parent.mkdir(parents=True, exist_ok=True) config_file.touch(mode=0o0600) if self.option("list"): self._list_configuration(config.all(), config.raw()) return 0 setting_key = self.argument("key") if not setting_key: return 0 if self.argument("value") and self.option("unset"): raise RuntimeError( "You can not combine a setting value with --unset") # show the value if no value is provided if not self.argument("value") and not self.option("unset"): m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): value = {} if config.get("repositories") is not None: value = config.get("repositories") else: repo = config.get("repositories.{}".format(m.group(1))) if repo is None: raise ValueError( "There is no {} repository defined".format( m.group(1))) value = repo self.line(str(value)) else: values = self.unique_config_values if setting_key not in values: raise ValueError( "There is no {} setting.".format(setting_key)) value = config.get(setting_key) if not isinstance(value, basestring): value = json.dumps(value) self.line(value) return 0 values = self.argument("value") unique_config_values = self.unique_config_values if setting_key in unique_config_values: if self.option("unset"): return config.config_source.remove_property(setting_key) return self._handle_single_value( config.config_source, setting_key, unique_config_values[setting_key], values, ) # handle repositories m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): raise ValueError( "You cannot remove the [repositories] section") if self.option("unset"): repo = config.get("repositories.{}".format(m.group(1))) if repo is None: raise ValueError( "There is no {} repository defined".format(m.group(1))) config.config_source.remove_property("repositories.{}".format( m.group(1))) return 0 if len(values) == 1: url = values[0] config.config_source.add_property( "repositories.{}.url".format(m.group(1)), url) return 0 raise ValueError( "You must pass the url. " "Example: poetry config repositories.foo https://bar.com") # handle auth m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key")) if m: if self.option("unset"): keyring_repository_password_del(config, m.group(2)) config.auth_config_source.remove_property("{}.{}".format( m.group(1), m.group(2))) return 0 if m.group(1) == "http-basic": if len(values) == 1: username = values[0] # Only username, so we prompt for password password = self.secret("Password:"******"Expected one or two arguments " "(username, password), got {}".format( len(values))) else: username = values[0] password = values[1] property_value = dict(username=username) try: keyring_repository_password_set(m.group(2), username, password) except RuntimeError: property_value.update(password=password) config.auth_config_source.add_property( "{}.{}".format(m.group(1), m.group(2)), property_value) elif m.group(1) == "pypi-token": if len(values) != 1: raise ValueError( "Expected only one argument (token), got {}".format( len(values))) token = values[0] config.auth_config_source.add_property( "{}.{}".format(m.group(1), m.group(2)), token) return 0 # handle certs m = re.match(r"(?:certificates)\.([^.]+)\.(cert|client-cert)", self.argument("key")) if m: if self.option("unset"): config.auth_config_source.remove_property( "certificates.{}.{}".format(m.group(1), m.group(2))) return 0 if len(values) == 1: config.auth_config_source.add_property( "certificates.{}.{}".format(m.group(1), m.group(2)), values[0]) else: raise ValueError("You must pass exactly 1 value") return 0 raise ValueError("Setting {} does not exist".format( self.argument("key"))) def _handle_single_value(self, source, key, callbacks, values): validator, normalizer, _ = callbacks if len(values) > 1: raise RuntimeError("You can only pass one value.") value = values[0] if not validator(value): raise RuntimeError('"{}" is an invalid value for {}'.format( value, key)) source.add_property(key, normalizer(value)) return 0 def _list_configuration(self, config, raw, k=""): from poetry.utils._compat import basestring orig_k = k for key, value in sorted(config.items()): if k + key in self.LIST_PROHIBITED_SETTINGS: continue raw_val = raw.get(key) if isinstance(value, dict): k += "{}.".format(key) self._list_configuration(value, raw_val, k=k) k = orig_k continue elif isinstance(value, list): value = [ json.dumps(val) if isinstance(val, list) else val for val in value ] value = "[{}]".format(", ".join(value)) if k.startswith("repositories."): message = "<c1>{}</c1> = <c2>{}</c2>".format( k + key, json.dumps(raw_val)) elif isinstance(raw_val, basestring) and raw_val != value: message = "<c1>{}</c1> = <c2>{}</c2> # {}".format( k + key, json.dumps(raw_val), value) else: message = "<c1>{}</c1> = <c2>{}</c2>".format( k + key, json.dumps(value)) self.line(message) def _list_setting(self, contents, setting=None, k=None, default=None): values = self._get_setting(contents, setting, k, default) for value in values: self.line("<comment>{}</comment> = <info>{}</info>".format( value[0], value[1])) def _get_setting(self, contents, setting=None, k=None, default=None): orig_k = k if setting and setting.split(".")[0] not in contents: value = json.dumps(default) return [((k or "") + setting, value)] else: values = [] for key, value in contents.items(): if setting and key != setting.split(".")[0]: continue if isinstance(value, dict) or key == "repositories" and k is None: if k is None: k = "" k += re.sub(r"^config\.", "", key + ".") if setting and len(setting) > 1: setting = ".".join(setting.split(".")[1:]) values += self._get_setting(value, k=k, setting=setting, default=default) k = orig_k continue if isinstance(value, list): value = [ json.dumps(val) if isinstance(val, list) else val for val in value ] value = "[{}]".format(", ".join(value)) value = json.dumps(value) values.append(((k or "") + key, value)) return values def _get_formatted_value(self, value): if isinstance(value, list): value = [ json.dumps(val) if isinstance(val, list) else val for val in value ] value = "[{}]".format(", ".join(value)) return json.dumps(value)
class VersionCommand(Command): name = "version" description = ("Shows the version of the project or bumps it when a valid " "bump rule is provided.") arguments = [ argument( "version", "The version number or the rule to update the version.", optional=True, ) ] options = [option("short", "s", "Output the version number only")] help = """\ The version command shows the current version of the project or bumps the version of the project and writes the new version back to <comment>pyproject.toml</> if a valid bump rule is provided. The new version should ideally be a valid semver string or a valid bump rule: patch, minor, major, prepatch, preminor, premajor, prerelease. """ RESERVED = { "major", "minor", "patch", "premajor", "preminor", "prepatch", "prerelease", } def handle(self): version = self.argument("version") if version: version = self.increment_version( self.poetry.package.pretty_version, version) if self.option("short"): self.line("{}".format(version)) else: self.line( "Bumping version from <b>{}</> to <fg=green>{}</>".format( self.poetry.package.pretty_version, version)) content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] poetry_content["version"] = version.text self.poetry.file.write(content) else: if self.option("short"): self.line("{}".format(self.poetry.package.pretty_version)) else: self.line("<comment>{}</> <info>{}</>".format( self.poetry.package.name, self.poetry.package.pretty_version)) def increment_version(self, version, rule): from poetry.core.semver import Version try: version = Version.parse(version) except ValueError: raise ValueError( "The project's version doesn't seem to follow semver") if rule in {"major", "premajor"}: new = version.next_major if rule == "premajor": new = new.first_prerelease elif rule in {"minor", "preminor"}: new = version.next_minor if rule == "preminor": new = new.first_prerelease elif rule in {"patch", "prepatch"}: new = version.next_patch if rule == "prepatch": new = new.first_prerelease elif rule == "prerelease": if version.is_prerelease(): pre = version.prerelease new_prerelease = int(pre[1]) + 1 new = Version.parse("{}.{}.{}-{}".format( version.major, version.minor, version.patch, ".".join([pre[0], str(new_prerelease)]), )) else: new = version.next_patch.first_prerelease else: new = Version.parse(rule) return new
class InstallCommand(EnvCommand): name = "install" description = "Installs the project dependencies." options = [ option("no-dev", None, "Do not install dev dependencies."), option( "no-root", None, "Do not install the root package (the current project)." ), option( "dry-run", None, "Outputs the operations but will not execute anything " "(implicitly enables --verbose).", ), option( "extras", "E", "Extra sets of dependencies to install.", flag=False, multiple=True, ), option( "develop", None, "Install given packages in development mode.", flag=False, multiple=True, ), ] help = """The <info>install</info> command reads the <comment>poetry.lock</> file from the current directory, processes it, and downloads and installs all the libraries and dependencies outlined in that file. If the file does not exist it will look for <comment>pyproject.toml</> and do the same. <info>poetry install</info> """ _loggers = ["poetry.repositories.pypi_repository"] def handle(self): from clikit.io import NullIO from poetry.installation import Installer from poetry.masonry.builders import EditableBuilder from poetry.masonry.utils.module import ModuleOrPackageNotFound installer = Installer( self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool ) extras = [] for extra in self.option("extras"): if " " in extra: extras += [e.strip() for e in extra.split(" ")] else: extras.append(extra) installer.extras(extras) installer.dev_mode(not self.option("no-dev")) installer.develop(self.option("develop")) installer.dry_run(self.option("dry-run")) installer.verbose(self.option("verbose")) return_code = installer.run() if return_code != 0: return return_code if self.option("no-root"): return 0 try: builder = EditableBuilder(self.poetry, self._env, NullIO()) except ModuleOrPackageNotFound: # This is likely due to the fact that the project is an application # not following the structure expected by Poetry # If this is a true error it will be picked up later by build anyway. return 0 self.line( " - Installing <info>{}</info> (<comment>{}</comment>)".format( self.poetry.package.pretty_name, self.poetry.package.pretty_version ) ) if self.option("dry-run"): return 0 builder.build() return 0
class AddCommand(InstallerCommand, InitCommand): name = "add" description = "Adds a new dependency to <comment>pyproject.toml</>." arguments = [argument("name", "The packages to add.", multiple=True)] options = [ option("dev", "D", "Add as a development dependency."), option( "extras", "E", "Extras to activate for the dependency.", flag=False, multiple=True, ), option("optional", None, "Add as an optional dependency."), option( "python", None, "Python version for which the dependency must be installed.", flag=False, ), option( "platform", None, "Platforms for which the dependency must be installed.", flag=False, ), option( "source", None, "Name of the source to use to install the package.", flag=False, ), option("allow-prereleases", None, "Accept prereleases."), option( "dry-run", None, "Output the operations but do not execute anything (implicitly enables --verbose).", ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] help = ( "The add command adds required packages to your <comment>pyproject.toml</> and installs them.\n\n" "If you do not specify a version constraint, poetry will choose a suitable one based on the available package versions.\n\n" "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" ) loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] def handle(self): from poetry.core.semver import parse_constraint from tomlkit import inline_table packages = self.argument("name") is_dev = self.option("dev") if self.option("extras") and len(packages) > 1: raise ValueError("You can only specify one package " "when using the --extras option") section = "dependencies" if is_dev: section = "dev-dependencies" original_content = self.poetry.file.read() content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] if section not in poetry_content: poetry_content[section] = {} for name in packages: for key in poetry_content[section]: if key.lower() == name.lower(): pair = self._parse_requirements([name])[0] if ("git" in pair or "url" in pair or pair.get("version") == "latest"): continue raise ValueError( "Package {} is already present".format(name)) requirements = self._determine_requirements( packages, allow_prereleases=self.option("allow-prereleases"), source=self.option("source"), ) for _constraint in requirements: if "version" in _constraint: # Validate version constraint parse_constraint(_constraint["version"]) constraint = inline_table() for name, value in _constraint.items(): if name == "name": continue constraint[name] = value if self.option("optional"): constraint["optional"] = True if self.option("allow-prereleases"): constraint["allow-prereleases"] = True if self.option("extras"): extras = [] for extra in self.option("extras"): if " " in extra: extras += [e.strip() for e in extra.split(" ")] else: extras.append(extra) constraint["extras"] = self.option("extras") if self.option("python"): constraint["python"] = self.option("python") if self.option("platform"): constraint["platform"] = self.option("platform") if self.option("source"): constraint["source"] = self.option("source") if len(constraint) == 1 and "version" in constraint: constraint = constraint["version"] poetry_content[section][_constraint["name"]] = constraint # Write new content self.poetry.file.write(content) # Cosmetic new line self.line("") # Update packages self.reset_poetry() self._installer.set_package(self.poetry.package) self._installer.dry_run(self.option("dry-run")) self._installer.verbose(self._io.is_verbose()) self._installer.update(True) if self.option("lock"): self._installer.lock() self._installer.whitelist([r["name"] for r in requirements]) try: status = self._installer.run() except Exception: self.poetry.file.write(original_content) raise if status != 0 or self.option("dry-run"): # Revert changes if not self.option("dry-run"): self.line_error( "\n" "<error>Failed to add packages, reverting the pyproject.toml file " "to its original content.</error>") self.poetry.file.write(original_content) return status
class RemoveCommand(EnvCommand): name = "remove" description = "Removes a package from the project dependencies." arguments = [ argument("packages", "Packages that should be removed", multiple=True) ] options = [ option("dev", "D", "Removes a package from the development dependencies."), option( "dry-run", None, "Outputs the operations but will not execute anything " "(implicitly enables --verbose).", ), ] help = """The <info>remove</info> command removes a package from the current list of installed packages <info>poetry remove</info>""" loggers = ["poetry.repositories.pypi_repository"] def handle(self): from poetry.installation import Installer packages = self.argument("packages") is_dev = self.option("dev") original_content = self.poetry.file.read() content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] section = "dependencies" if is_dev: section = "dev-dependencies" # Deleting entries requirements = {} for name in packages: found = False for key in poetry_content[section]: if key.lower() == name.lower(): found = True requirements[key] = poetry_content[section][key] break if not found: raise ValueError("Package {} not found".format(name)) for key in requirements: del poetry_content[section][key] # Write the new content back self.poetry.file.write(content) # Update packages self.reset_poetry() installer = Installer(self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool) installer.dry_run(self.option("dry-run")) installer.update(True) installer.whitelist(requirements) try: status = installer.run() except Exception: self.poetry.file.write(original_content) raise if status != 0 or self.option("dry-run"): # Revert changes if not self.option("dry-run"): self.error("\n" "Removal failed, reverting pyproject.toml " "to its original content.") self.poetry.file.write(original_content) return status
class SelfUpdateCommand(Command): name = "update" description = "Updates Poetry to the latest version." arguments = [ argument("version", "The version to update to.", optional=True) ] options = [option("preview", None, "Install prereleases.")] REPOSITORY_URL = "https://github.com/python-poetry/poetry" BASE_URL = REPOSITORY_URL + "/releases/download" @property def home(self): # type: () -> Path from pathlib import Path return Path(os.environ.get("POETRY_HOME", "~/.poetry")).expanduser() @property def bin(self): # type: () -> Path return self.home / "bin" @property def lib(self): # type: () -> Path return self.home / "lib" @property def lib_backup(self): # type: () -> Path return self.home / "lib-backup" def handle(self): # type: () -> None from poetry.__version__ import __version__ from poetry.core.semver import Version from poetry.repositories.pypi_repository import PyPiRepository self._check_recommended_installation() version = self.argument("version") if not version: version = ">=" + __version__ repo = PyPiRepository(fallback=False) packages = repo.find_packages( Dependency("poetry", version, allows_prereleases=self.option("preview"))) if not packages: self.line("No release found for the specified version") return packages.sort(key=cmp_to_key(lambda x, y: 0 if x.version == y.version else int(x.version < y.version or -1))) release = None for package in packages: if package.is_prerelease(): if self.option("preview"): release = package break continue release = package break if release is None: self.line("No new release found") return if release.version == Version.parse(__version__): self.line("You are using the latest version") return self.update(release) def update(self, release): # type: ("Package") -> None version = release.version self.line("Updating to <info>{}</info>".format(version)) if self.lib_backup.exists(): shutil.rmtree(str(self.lib_backup)) # Backup the current installation if self.lib.exists(): shutil.copytree(str(self.lib), str(self.lib_backup)) shutil.rmtree(str(self.lib)) try: self._update(version) except Exception: if not self.lib_backup.exists(): raise shutil.copytree(str(self.lib_backup), str(self.lib)) shutil.rmtree(str(self.lib_backup)) raise finally: if self.lib_backup.exists(): shutil.rmtree(str(self.lib_backup)) self.make_bin() self.line("") self.line("") self.line( "<info>Poetry</info> (<comment>{}</comment>) is installed now. Great!" .format(version)) def _update(self, version): # type: ("Version") -> None from poetry.utils.helpers import temporary_directory release_name = self._get_release_name(version) checksum = "{}.sha256sum".format(release_name) base_url = self.BASE_URL try: r = urlopen(base_url + "/{}/{}".format(version, checksum)) except HTTPError as e: if e.code == 404: raise RuntimeError("Could not find {} file".format(checksum)) raise checksum = r.read().decode().strip() # We get the payload from the remote host name = "{}.tar.gz".format(release_name) try: r = urlopen(base_url + "/{}/{}".format(version, name)) except HTTPError as e: if e.code == 404: raise RuntimeError("Could not find {} file".format(name)) raise meta = r.info() size = int(meta["Content-Length"]) current = 0 block_size = 8192 bar = self.progress_bar(max=size) bar.set_format( " - Downloading <info>{}</> <comment>%percent%%</>".format(name)) bar.start() sha = hashlib.sha256() with temporary_directory(prefix="poetry-updater-") as dir_: tar = os.path.join(dir_, name) with open(tar, "wb") as f: while True: buffer = r.read(block_size) if not buffer: break current += len(buffer) f.write(buffer) sha.update(buffer) bar.set_progress(current) bar.finish() # Checking hashes if checksum != sha.hexdigest(): raise RuntimeError( "Hashes for {} do not match: {} != {}".format( name, checksum, sha.hexdigest())) gz = GzipFile(tar, mode="rb") try: with tarfile.TarFile(tar, fileobj=gz, format=tarfile.PAX_FORMAT) as f: f.extractall(str(self.lib)) finally: gz.close() def process(self, *args): # type: (*Any) -> str return subprocess.check_output(list(args), stderr=subprocess.STDOUT) def _check_recommended_installation(self): # type: () -> None from pathlib import Path current = Path(__file__) try: current.relative_to(self.home) except ValueError: raise RuntimeError( "Poetry was not installed with the recommended installer. " "Cannot update automatically.") def _get_release_name(self, version): # type: ("Version") -> str platform = sys.platform if platform == "linux2": platform = "linux" return "poetry-{}-{}".format(version, platform) def make_bin(self): # type: () -> None from poetry.utils._compat import WINDOWS self.bin.mkdir(0o755, parents=True, exist_ok=True) python_executable = self._which_python() if WINDOWS: with self.bin.joinpath("poetry.bat").open("w", newline="") as f: f.write( BAT.format( python_executable=python_executable, poetry_bin=str(self.bin / "poetry").replace( os.environ["USERPROFILE"], "%USERPROFILE%"), )) bin_content = BIN if not WINDOWS: bin_content = "#!/usr/bin/env {}\n".format( python_executable) + bin_content self.bin.joinpath("poetry").write_text(bin_content, encoding="utf-8") if not WINDOWS: # Making the file executable st = os.stat(str(self.bin.joinpath("poetry"))) os.chmod(str(self.bin.joinpath("poetry")), st.st_mode | stat.S_IEXEC) def _which_python(self): # type: () -> str """ Decides which python executable we'll embed in the launcher script. """ from poetry.utils._compat import WINDOWS allowed_executables = ["python", "python3"] if WINDOWS: allowed_executables += ["py.exe -3", "py.exe -2"] # \d in regex ensures we can convert to int later version_matcher = re.compile( r"^Python (?P<major>\d+)\.(?P<minor>\d+)\..+$") fallback = None for executable in allowed_executables: try: raw_version = subprocess.check_output( executable + " --version", stderr=subprocess.STDOUT, shell=True).decode("utf-8") except subprocess.CalledProcessError: continue match = version_matcher.match(raw_version.strip()) if match and tuple(map(int, match.groups())) >= (3, 0): # favor the first py3 executable we can find. return executable if fallback is None: # keep this one as the fallback; it was the first valid executable we found. fallback = executable if fallback is None: # Avoid breaking existing scripts fallback = "python" return fallback
class ShowCommand(EnvCommand): name = "show" description = "Shows information about packages." arguments = [argument("package", "The package to inspect", optional=True)] options = [ option("no-dev", None, "Do not list the development dependencies."), option("tree", "t", "List the dependencies as a tree."), option("latest", "l", "Show the latest version."), option( "outdated", "o", "Show the latest version but only for packages that are outdated.", ), option( "all", "a", "Show all packages (even those not compatible with current system).", ), ] help = """The show command displays detailed information about a package, or lists all packages available.""" colors = ["cyan", "yellow", "green", "magenta", "blue"] def handle(self): from clikit.utils.terminal import Terminal from poetry.io.null_io import NullIO from poetry.puzzle.solver import Solver from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository from poetry.utils.helpers import get_package_version_display_string package = self.argument("package") if self.option("tree"): self.init_styles(self.io) if self.option("outdated"): self._args.set_option("latest", True) include_dev = not self.option("no-dev") locked_repo = self.poetry.locker.locked_repository(True) # Show tree view if requested if self.option("tree") and not package: requires = self.poetry.package.requires + self.poetry.package.dev_requires packages = locked_repo.packages for package in packages: for require in requires: if package.name == require.name: self.display_package_tree(self._io, package, locked_repo) break return 0 table = self.table(style="compact") # table.style.line_vc_char = "" locked_packages = locked_repo.packages pool = Pool(ignore_repository_names=True) pool.add_repository(locked_repo) solver = Solver( self.poetry.package, pool=pool, installed=Repository(), locked=locked_repo, io=NullIO(), ) solver.provider.load_deferred(False) with solver.use_environment(self.env): ops = solver.solve() required_locked_packages = set( [op.package for op in ops if not op.skipped]) if self.option("no-dev"): required_locked_packages = [ p for p in locked_packages if p.category == "main" ] if package: pkg = None for locked in locked_packages: if package.lower() == locked.name: pkg = locked break if not pkg: raise ValueError("Package {} not found".format(package)) if self.option("tree"): self.display_package_tree(self.io, pkg, locked_repo) return 0 rows = [ ["<info>name</>", " : <c1>{}</>".format(pkg.pretty_name)], [ "<info>version</>", " : <b>{}</b>".format(pkg.pretty_version) ], ["<info>description</>", " : {}".format(pkg.description)], ] table.add_rows(rows) table.render(self.io) if pkg.requires: self.line("") self.line("<info>dependencies</info>") for dependency in pkg.requires: self.line(" - <c1>{}</c1> <b>{}</b>".format( dependency.pretty_name, dependency.pretty_constraint)) return 0 show_latest = self.option("latest") show_all = self.option("all") terminal = Terminal() width = terminal.width name_length = version_length = latest_length = 0 latest_packages = {} latest_statuses = {} installed_repo = InstalledRepository.load(self.env) # Computing widths for locked in locked_packages: if locked not in required_locked_packages and not show_all: continue current_length = len(locked.pretty_name) if not self._io.output.supports_ansi(): installed_status = self.get_installed_status( locked, installed_repo) if installed_status == "not-installed": current_length += 4 if show_latest: latest = self.find_latest_package(locked, include_dev) if not latest: latest = locked latest_packages[locked.pretty_name] = latest update_status = latest_statuses[ locked.pretty_name] = self.get_update_status( latest, locked) if not self.option( "outdated") or update_status != "up-to-date": name_length = max(name_length, current_length) version_length = max( version_length, len( get_package_version_display_string( locked, root=self.poetry.file.parent)), ) latest_length = max( latest_length, len( get_package_version_display_string( latest, root=self.poetry.file.parent)), ) else: name_length = max(name_length, current_length) version_length = max( version_length, len( get_package_version_display_string( locked, root=self.poetry.file.parent)), ) write_version = name_length + version_length + 3 <= width write_latest = name_length + version_length + latest_length + 3 <= width write_description = name_length + version_length + latest_length + 24 <= width for locked in locked_packages: color = "cyan" name = locked.pretty_name install_marker = "" if locked not in required_locked_packages: if not show_all: continue color = "black;options=bold" else: installed_status = self.get_installed_status( locked, installed_repo) if installed_status == "not-installed": color = "red" if not self._io.output.supports_ansi(): # Non installed in non decorated mode install_marker = " (!)" if (show_latest and self.option("outdated") and latest_statuses[locked.pretty_name] == "up-to-date"): continue line = "<fg={}>{:{}}{}</>".format( color, name, name_length - len(install_marker), install_marker) if write_version: line += " <b>{:{}}</b>".format( get_package_version_display_string( locked, root=self.poetry.file.parent), version_length, ) if show_latest: latest = latest_packages[locked.pretty_name] update_status = latest_statuses[locked.pretty_name] if write_latest: color = "green" if update_status == "semver-safe-update": color = "red" elif update_status == "update-possible": color = "yellow" line += " <fg={}>{:{}}</>".format( color, get_package_version_display_string( latest, root=self.poetry.file.parent), latest_length, ) if write_description: description = locked.description remaining = width - name_length - version_length - 4 if show_latest: remaining -= latest_length if len(locked.description) > remaining: description = description[:remaining - 3] + "..." line += " " + description self.line(line) def display_package_tree(self, io, package, installed_repo): io.write("<c1>{}</c1>".format(package.pretty_name)) description = "" if package.description: description = " " + package.description io.write_line(" <b>{}</b>{}".format(package.pretty_version, description)) dependencies = package.requires dependencies = sorted(dependencies, key=lambda x: x.name) tree_bar = "├" j = 0 total = len(dependencies) for dependency in dependencies: j += 1 if j == total: tree_bar = "└" level = 1 color = self.colors[level] info = "{tree_bar}── <{color}>{name}</{color}> {constraint}".format( tree_bar=tree_bar, color=color, name=dependency.name, constraint=dependency.pretty_constraint, ) self._write_tree_line(io, info) tree_bar = tree_bar.replace("└", " ") packages_in_tree = [package.name, dependency.name] self._display_tree(io, dependency, installed_repo, packages_in_tree, tree_bar, level + 1) def _display_tree( self, io, dependency, installed_repo, packages_in_tree, previous_tree_bar="├", level=1, ): previous_tree_bar = previous_tree_bar.replace("├", "│") dependencies = [] for package in installed_repo.packages: if package.name == dependency.name: dependencies = package.requires break dependencies = sorted(dependencies, key=lambda x: x.name) tree_bar = previous_tree_bar + " ├" i = 0 total = len(dependencies) for dependency in dependencies: i += 1 current_tree = packages_in_tree if i == total: tree_bar = previous_tree_bar + " └" color_ident = level % len(self.colors) color = self.colors[color_ident] circular_warn = "" if dependency.name in current_tree: circular_warn = "(circular dependency aborted here)" info = "{tree_bar}── <{color}>{name}</{color}> {constraint} {warn}".format( tree_bar=tree_bar, color=color, name=dependency.name, constraint=dependency.pretty_constraint, warn=circular_warn, ) self._write_tree_line(io, info) tree_bar = tree_bar.replace("└", " ") if dependency.name not in current_tree: current_tree.append(dependency.name) self._display_tree(io, dependency, installed_repo, current_tree, tree_bar, level + 1) def _write_tree_line(self, io, line): if not io.output.supports_ansi(): line = line.replace("└", "`-") line = line.replace("├", "|-") line = line.replace("──", "-") line = line.replace("│", "|") io.write_line(line) def init_styles(self, io): from clikit.api.formatter import Style for color in self.colors: style = Style(color).fg(color) io.output.formatter.add_style(style) io.error_output.formatter.add_style(style) def find_latest_package(self, package, include_dev): from clikit.io import NullIO from poetry.puzzle.provider import Provider from poetry.version.version_selector import VersionSelector # find the latest version allowed in this pool if package.source_type in ("git", "file", "directory"): requires = self.poetry.package.requires if include_dev: requires = requires + self.poetry.package.dev_requires for dep in requires: if dep.name == package.name: provider = Provider(self.poetry.package, self.poetry.pool, NullIO()) if dep.is_vcs(): return provider.search_for_vcs(dep)[0] if dep.is_file(): return provider.search_for_file(dep)[0] if dep.is_directory(): return provider.search_for_directory(dep)[0] name = package.name selector = VersionSelector(self.poetry.pool) return selector.find_best_candidate( name, ">={}".format(package.pretty_version)) def get_update_status(self, latest, package): from poetry.core.semver import parse_constraint if latest.full_pretty_version == package.full_pretty_version: return "up-to-date" constraint = parse_constraint("^" + package.pretty_version) if latest.version and constraint.allows(latest.version): # It needs an immediate semver-compliant upgrade return "semver-safe-update" # it needs an upgrade but has potential BC breaks so is not urgent return "update-possible" def get_installed_status(self, locked, installed_repo): for package in installed_repo.packages: if locked.name == package.name: return "installed" return "not-installed"
class DebugResolveCommand(Command): name = "resolve" description = "Debugs dependency resolution." arguments = [ argument("package", "The packages to resolve.", optional=True, multiple=True) ] options = [ option( "extras", "E", "Extras to activate for the dependency.", flag=False, multiple=True, ), option("python", None, "Python version(s) to use for resolution.", flag=False), option("tree", None, "Display the dependency tree."), option("install", None, "Show what would be installed for the current system."), ] loggers = ["poetry.repositories.pypi_repository"] def handle(self): from poetry.packages import ProjectPackage from poetry.puzzle import Solver from poetry.repositories.repository import Repository from poetry.semver import parse_constraint from poetry.utils.env import EnvManager packages = self.argument("package") if not packages: package = self.poetry.package else: package = ProjectPackage(self.poetry.package.name, self.poetry.package.version) requirements = self._format_requirements(packages) for name, constraint in requirements.items(): dep = package.add_dependency(name, constraint) extras = [] for extra in self.option("extras"): if " " in extra: extras += [e.strip() for e in extra.split(" ")] else: extras.append(extra) for ex in extras: dep.extras.append(ex) package.python_versions = self.option("python") or ( self.poetry.package.python_versions) pool = self.poetry.pool solver = Solver(package, pool, Repository(), Repository(), self._io) ops = solver.solve() self.line("") self.line("Resolution results:") self.line("") if self.option("tree"): show_command = self.application.find("show") show_command.init_styles(self.io) packages = [op.package for op in ops] repo = Repository(packages) requires = package.requires + package.dev_requires for pkg in repo.packages: for require in requires: if pkg.name == require.name: show_command.display_package_tree(self.io, pkg, repo) break return 0 env = EnvManager(self.poetry.config).get(self.poetry.file.parent) current_python_version = parse_constraint(".".join( str(v) for v in env.version_info)) table = self.table([], style="borderless") rows = [] for op in ops: pkg = op.package if self.option("install"): if not pkg.python_constraint.allows( current_python_version) or not env.is_valid_for_marker( pkg.marker): continue row = [ "<info>{}</info>".format(pkg.name), "<b>{}</b>".format(pkg.version), "", ] if not pkg.marker.is_any(): row[2] = str(pkg.marker) rows.append(row) table.set_rows(rows) table.render(self.io) def _determine_requirements(self, requires): # type: (List[str]) -> List[str] from poetry.semver import parse_constraint if not requires: return [] requires = self._parse_name_version_pairs(requires) for requirement in requires: if "version" in requirement: parse_constraint(requirement["version"]) return requires def _parse_name_version_pairs(self, pairs): # type: (list) -> list result = [] for i in range(len(pairs)): if pairs[i].startswith("git+https://"): url = pairs[i].lstrip("git+") rev = None if "@" in url: url, rev = url.split("@") pair = {"name": url.split("/")[-1].rstrip(".git"), "git": url} if rev: pair["rev"] = rev result.append(pair) continue 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, "version": "*"}) return result def _format_requirements(self, requirements): # type: (List[str]) -> dict requires = {} requirements = self._determine_requirements(requirements) for requirement in requirements: name = requirement.pop("name") requires[name] = requirement return requires