def test_builder_installs_proper_files_when_packages_configured( project_with_include: Poetry, tmp_venv: VirtualEnv): builder = EditableBuilder(project_with_include, tmp_venv, NullIO()) builder.build() pth_file = "with_include.pth" assert tmp_venv.site_packages.exists(pth_file) pth_file = tmp_venv.site_packages.find(pth_file)[0] paths = set() with pth_file.open() as f: for line in f.readlines(): line = line.strip(os.linesep) if line: paths.add(line) project_root = project_with_include.file.parent.resolve() expected = { project_root.as_posix(), project_root.joinpath("src").as_posix() } assert paths.issubset(expected) assert len(paths) == len(expected)
def test_builder_setup_generation_runs_with_pip_editable(tmp_dir: str): # create an isolated copy of the project fixture = Path( __file__).parent.parent.parent / "fixtures" / "extended_project" extended_project = Path(tmp_dir) / "extended_project" shutil.copytree(fixture, extended_project) assert extended_project.exists() poetry = Factory().create_poetry(extended_project) # we need a venv with setuptools since we are verifying setup.py builds with ephemeral_environment(flags={"no-setuptools": False}) as venv: builder = EditableBuilder(poetry, venv, NullIO()) builder.build() # is the package installed? repository = InstalledRepository.load(venv) assert repository.package("extended-project", "1.2.3") # check for the module built by build.py try: output = venv.run_python_script( "from extended_project import built; print(built.__file__)" ).strip() except EnvCommandError: pytest.fail("Unable to import built module") else: built_py = Path(output).resolve() expected = extended_project / "extended_project" / "built.py" # ensure the package was installed as editable assert built_py == expected.resolve()
def test_builder_should_execute_build_scripts(extended_without_setup_poetry): env = MockEnv(path=Path("/foo")) builder = EditableBuilder(extended_without_setup_poetry, env, NullIO()) builder.build() assert [ ["python", str(extended_without_setup_poetry.file.parent / "build.py")] ] == env.executed
def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts( mocker, extended_poetry, tmp_dir): pip_editable_install = mocker.patch( "poetry.masonry.builders.editable.pip_editable_install") env = MockEnv(path=Path(tmp_dir) / "foo") builder = EditableBuilder(extended_poetry, env, NullIO()) builder.build() pip_editable_install.assert_called_once_with( extended_poetry.pyproject.file.path.parent, env) assert [] == env.executed
def install_directory(self, package): from poetry.factory import Factory from poetry.utils.toml_file import TomlFile if package.root_dir: req = (package.root_dir / package.source_url).as_posix() else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = TomlFile(os.path.join(req, "pyproject.toml")) has_poetry = False has_build_system = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ( "tool" in pyproject_content and "poetry" in pyproject_content["tool"] ) # Even if there is a build system specified # pip as of right now does not support it fully # TODO: Check for pip version when proper PEP-517 support lands # has_build_system = ("build-system" in pyproject_content) setup = os.path.join(req, "setup.py") has_setup = os.path.exists(setup) if has_poetry and (package.develop or not has_build_system): # We actually need to rely on creating a temporary setup.py # file since pip, as of this comment, does not support # build-system for editable packages # We also need it for non-PEP-517 packages from poetry.masonry.builders.editable import EditableBuilder builder = EditableBuilder( Factory().create_poetry(pyproject.parent), self._env, NullIO() ) builder.build() return if package.develop: args.append("-e") args.append(req) try: return self.run(*args) finally: if not has_setup and os.path.exists(setup): os.remove(setup)
def test_builder_should_execute_build_scripts( mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str): env = MockEnv(path=Path(tmp_dir) / "foo") mocker.patch("poetry.masonry.builders.editable.build_environment" ).return_value.__enter__.return_value = env builder = EditableBuilder(extended_without_setup_poetry, env, NullIO()) builder.build() assert [[ "python", str(extended_without_setup_poetry.file.parent / "build.py") ]] == env.executed
def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts( extended_poetry, tmp_dir): env = MockEnv(path=Path(tmp_dir) / "foo") builder = EditableBuilder(extended_poetry, env, NullIO()) builder.build() assert [[ "python", "-m", "pip", "install", "-e", str(extended_poetry.file.parent), "--no-deps", ]] == env.executed
def _install_directory(self, operation: Install | Update) -> int: from poetry.factory import Factory package = operation.package operation_message = self.get_operation_message(operation) message = ( f" <fg=blue;options=bold>•</> {operation_message}:" " <info>Building...</info>" ) self._write(operation, message) assert package.source_url is not None if package.root_dir: req = package.root_dir / package.source_url else: req = Path(package.source_url).resolve(strict=False) pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) if pyproject.is_poetry_project(): # Even if there is a build system specified # some versions of pip (< 19.0.0) don't understand it # so we need to check the version of pip to know # if we can rely on the build system legacy_pip = ( self._env.pip_version < self._env.pip_version.__class__.from_parts(19, 0, 0) ) package_poetry = Factory().create_poetry(pyproject.file.path.parent) builder: Builder if package.develop and not package_poetry.package.build_script: from poetry.masonry.builders.editable import EditableBuilder # This is a Poetry package in editable mode # we can use the EditableBuilder without going through pip # to install it, unless it has a build script. builder = EditableBuilder(package_poetry, self._env, NullIO()) builder.build() return 0 elif legacy_pip or package_poetry.package.build_script: from poetry.core.masonry.builders.sdist import SdistBuilder # We need to rely on creating a temporary setup.py # file since the version of pip does not support # build-systems # We also need it for non-PEP-517 packages builder = SdistBuilder(package_poetry) with builder.setup_py(): if package.develop: return self.pip_install(req, upgrade=True, editable=True) return self.pip_install(req, upgrade=True) if package.develop: return self.pip_install(req, upgrade=True, editable=True) return self.pip_install(req, upgrade=True)
def install_directory(self, package: "Package") -> Union[str, int]: from cleo.io.null_io import NullIO from poetry.factory import Factory req: Path if package.root_dir: req = (package.root_dir / package.source_url).as_posix() else: req = Path(package.source_url).resolve(strict=False) pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) if pyproject.is_poetry_project(): # Even if there is a build system specified # some versions of pip (< 19.0.0) don't understand it # so we need to check the version of pip to know # if we can rely on the build system legacy_pip = self._env.pip_version < self._env.pip_version.__class__( 19, 0, 0) package_poetry = Factory().create_poetry( pyproject.file.path.parent) if package.develop and not package_poetry.package.build_script: from poetry.masonry.builders.editable import EditableBuilder # This is a Poetry package in editable mode # we can use the EditableBuilder without going through pip # to install it, unless it has a build script. builder = EditableBuilder(package_poetry, self._env, NullIO()) builder.build() return 0 elif legacy_pip or package_poetry.package.build_script: from poetry.core.masonry.builders.sdist import SdistBuilder # We need to rely on creating a temporary setup.py # file since the version of pip does not support # build-systems # We also need it for non-PEP-517 packages builder = SdistBuilder(package_poetry) with builder.setup_py(): if package.develop: return pip_editable_install(directory=req, environment=self._env) return pip_install(path=req, environment=self._env, deps=False, upgrade=True) if package.develop: return pip_editable_install(directory=req, environment=self._env) return pip_install(path=req, environment=self._env, deps=False, upgrade=True)
def install_directory(self, package): from poetry.factory import Factory from poetry.io.null_io import NullIO if package.root_dir: req = (package.root_dir / package.source_url).as_posix() else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) if pyproject.is_poetry_project(): # Even if there is a build system specified # some versions of pip (< 19.0.0) don't understand it # so we need to check the version of pip to know # if we can rely on the build system legacy_pip = self._env.pip_version < self._env.pip_version.__class__( 19, 0, 0) package_poetry = Factory().create_poetry( pyproject.file.path.parent) if package.develop and not package_poetry.package.build_script: from poetry.masonry.builders.editable import EditableBuilder # This is a Poetry package in editable mode # we can use the EditableBuilder without going through pip # to install it, unless it has a build script. builder = EditableBuilder(package_poetry, self._env, NullIO()) builder.build() return 0 elif legacy_pip or package_poetry.package.build_script: from poetry.core.masonry.builders.sdist import SdistBuilder # We need to rely on creating a temporary setup.py # file since the version of pip does not support # build-systems # We also need it for non-PEP-517 packages builder = SdistBuilder(package_poetry) with builder.setup_py(): if package.develop: args.append("-e") args.append(req) return self.run_pip(*args) if package.develop: args.append("-e") args.append(req) return self.run(*args)
def test_builder_installs_proper_files_for_standard_packages( simple_poetry, tmp_venv): builder = EditableBuilder(simple_poetry, tmp_venv, NullIO()) builder.build() assert tmp_venv._bin_dir.joinpath("foo").exists() assert tmp_venv.site_packages.path.joinpath("simple_project.pth").exists() assert simple_poetry.file.parent.resolve().as_posix( ) == tmp_venv.site_packages.path.joinpath( "simple_project.pth").read_text().strip(os.linesep) dist_info = tmp_venv.site_packages.path.joinpath( "simple_project-1.2.3.dist-info") assert dist_info.exists() assert dist_info.joinpath("INSTALLER").exists() assert dist_info.joinpath("METADATA").exists() assert dist_info.joinpath("RECORD").exists() assert dist_info.joinpath("entry_points.txt").exists() assert "poetry" == dist_info.joinpath("INSTALLER").read_text() assert ( "[console_scripts]\nbaz=bar:baz.boom.bim\nfoo=foo:bar\nfox=fuz.foo:bar.baz\n\n" == dist_info.joinpath("entry_points.txt").read_text()) metadata = """\ Metadata-Version: 2.1 Name: simple-project Version: 1.2.3 Summary: Some description. Home-page: https://python-poetry.org License: MIT Keywords: packaging,dependency,poetry Author: Sébastien Eustace Author-email: [email protected] Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Project-URL: Documentation, https://python-poetry.org/docs Project-URL: Repository, https://github.com/python-poetry/poetry Description-Content-Type: text/x-rst My Package ========== """ assert metadata == dist_info.joinpath("METADATA").read_text( encoding="utf-8") records = dist_info.joinpath("RECORD").read_text() assert str( tmp_venv.site_packages.path.joinpath("simple_project.pth")) in records assert str(tmp_venv._bin_dir.joinpath("foo")) in records assert str(tmp_venv._bin_dir.joinpath("baz")) in records assert str(dist_info.joinpath("METADATA")) in records assert str(dist_info.joinpath("INSTALLER")) in records assert str(dist_info.joinpath("entry_points.txt")) in records assert str(dist_info.joinpath("RECORD")) in records baz_script = """\ #!{python} from bar import baz if __name__ == '__main__': baz.boom.bim() """.format(python=tmp_venv._bin("python")) assert baz_script == tmp_venv._bin_dir.joinpath("baz").read_text() foo_script = """\ #!{python} from foo import bar if __name__ == '__main__': bar() """.format(python=tmp_venv._bin("python")) assert foo_script == tmp_venv._bin_dir.joinpath("foo").read_text() fox_script = """\ #!{python} from fuz.foo import bar if __name__ == '__main__': bar.baz() """.format(python=tmp_venv._bin("python")) assert fox_script == tmp_venv._bin_dir.joinpath("fox").read_text()
def _install_directory(self, operation): from poetry.factory import Factory from poetry.utils.toml_file import TomlFile package = operation.package operation_message = self.get_operation_message(operation) message = " <fg=blue;options=bold>•</> {message}: <info>Building...</info>".format( message=operation_message, ) self._write(operation, message) if package.root_dir: req = os.path.join(str(package.root_dir), package.source_url) else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = TomlFile(os.path.join(req, "pyproject.toml")) has_poetry = False has_build_system = False if pyproject.exists(): pyproject_content = pyproject.read() has_poetry = ( "tool" in pyproject_content and "poetry" in pyproject_content["tool"] ) # Even if there is a build system specified # some versions of pip (< 19.0.0) don't understand it # so we need to check the version of pip to know # if we can rely on the build system pip_version = self._env.pip_version pip_version_with_build_system_support = pip_version.__class__(19, 0, 0) has_build_system = ( "build-system" in pyproject_content and pip_version >= pip_version_with_build_system_support ) if has_poetry: package_poetry = Factory().create_poetry(pyproject.parent) if package.develop and not package_poetry.package.build_script: from poetry.masonry.builders.editable import EditableBuilder # This is a Poetry package in editable mode # we can use the EditableBuilder without going through pip # to install it, unless it has a build script. builder = EditableBuilder(package_poetry, self._env, NullIO()) builder.build() return 0 elif not has_build_system or package_poetry.package.build_script: from poetry.core.masonry.builders.sdist import SdistBuilder # We need to rely on creating a temporary setup.py # file since the version of pip does not support # build-systems # We also need it for non-PEP-517 packages builder = SdistBuilder(package_poetry) with builder.setup_py(): if package.develop: args.append("-e") args.append(req) return self.run_pip(*args) if package.develop: args.append("-e") args.append(req) return self.run_pip(*args)
def _install_directory(self, operation: Union[Install, Update]) -> int: from poetry.factory import Factory package = operation.package operation_message = self.get_operation_message(operation) message = ( " <fg=blue;options=bold>•</> {message}: <info>Building...</info>". format(message=operation_message, )) self._write(operation, message) if package.root_dir: req = os.path.join(str(package.root_dir), package.source_url) else: req = os.path.realpath(package.source_url) args = ["install", "--no-deps", "-U"] pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) if pyproject.is_poetry_project(): # Even if there is a build system specified # some versions of pip (< 19.0.0) don't understand it # so we need to check the version of pip to know # if we can rely on the build system legacy_pip = self._env.pip_version < self._env.pip_version.__class__( 19, 0, 0) package_poetry = Factory().create_poetry( pyproject.file.path.parent) if package.develop and not package_poetry.package.build_script: from poetry.masonry.builders.editable import EditableBuilder # This is a Poetry package in editable mode # we can use the EditableBuilder without going through pip # to install it, unless it has a build script. builder = EditableBuilder(package_poetry, self._env, NullIO()) builder.build() return 0 elif legacy_pip or package_poetry.package.build_script: from poetry.core.masonry.builders.sdist import SdistBuilder # We need to rely on creating a temporary setup.py # file since the version of pip does not support # build-systems # We also need it for non-PEP-517 packages builder = SdistBuilder(package_poetry) with builder.setup_py(): if package.develop: args.append("-e") args.append(req) return self.run_pip(*args) if package.develop: args.append("-e") args.append(req) return self.run_pip(*args)
def handle(self) -> int: from poetry.core.masonry.utils.module import ModuleOrPackageNotFound from poetry.masonry.builders.editable import EditableBuilder self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False)) if self.option("extras") and self.option("all-extras"): self.line_error( "<error>You cannot specify explicit" " `<fg=yellow;options=bold>--extras</>` while installing" " using `<fg=yellow;options=bold>--all-extras</>`.</error>") return 1 if self.option("only-root") and any( self.option(key) for key in {"with", "without", "only"}): self.line_error( "<error>The `<fg=yellow;options=bold>--with</>`," " `<fg=yellow;options=bold>--without</>` and" " `<fg=yellow;options=bold>--only</>` options cannot be used with" " the `<fg=yellow;options=bold>--only-root</>`" " option.</error>") return 1 if self.option("only-root") and self.option("no-root"): self.line_error( "<error>You cannot specify `<fg=yellow;options=bold>--no-root</>`" " when using `<fg=yellow;options=bold>--only-root</>`.</error>" ) return 1 if self.option("all-extras"): extras = list(self.poetry.package.extras.keys()) else: extras = [] for extra in self.option("extras", []): if " " in extra: extras += [e.strip() for e in extra.split(" ")] else: extras.append(extra) self._installer.extras(extras) with_synchronization = self.option("sync") if self.option("remove-untracked"): self.line_error( "<warning>The `<fg=yellow;options=bold>--remove-untracked</>` option is" " deprecated, use the `<fg=yellow;options=bold>--sync</>` option" " instead.</warning>") with_synchronization = True self._installer.only_groups(self.activated_groups) self._installer.dry_run(self.option("dry-run")) self._installer.requires_synchronization(with_synchronization) self._installer.verbose(self.io.is_verbose()) return_code = self._installer.run() if return_code != 0: return return_code if self.option("no-root"): return 0 try: builder = EditableBuilder(self.poetry, self._env, self.io) 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 log_install = ("<b>Installing</> the current project:" f" <c1>{self.poetry.package.pretty_name}</c1>" f" (<{{tag}}>{self.poetry.package.pretty_version}</>)") overwrite = self.io.output.is_decorated() and not self.io.is_debug() self.line("") self.write(log_install.format(tag="c2")) if not overwrite: self.line("") if self.option("dry-run"): self.line("") return 0 builder.build() if overwrite: self.overwrite(log_install.format(tag="success")) self.line("") return 0
def test_builder_installs_proper_files_for_standard_packages( simple_poetry: Poetry, tmp_venv: VirtualEnv): builder = EditableBuilder(simple_poetry, tmp_venv, NullIO()) builder.build() assert tmp_venv._bin_dir.joinpath("foo").exists() pth_file = "simple_project.pth" assert tmp_venv.site_packages.exists(pth_file) assert (simple_poetry.file.parent.resolve().as_posix() == tmp_venv.site_packages.find(pth_file)[0].read_text().strip( os.linesep)) dist_info = "simple_project-1.2.3.dist-info" assert tmp_venv.site_packages.exists(dist_info) dist_info = tmp_venv.site_packages.find(dist_info)[0] assert dist_info.joinpath("INSTALLER").exists() assert dist_info.joinpath("METADATA").exists() assert dist_info.joinpath("RECORD").exists() assert dist_info.joinpath("entry_points.txt").exists() assert dist_info.joinpath("direct_url.json").exists() assert not DeepDiff( { "dir_info": { "editable": True }, "url": simple_poetry.file.path.parent.as_uri(), }, json.loads(dist_info.joinpath("direct_url.json").read_text()), ) assert dist_info.joinpath("INSTALLER").read_text() == "poetry" assert (dist_info.joinpath("entry_points.txt").read_text() == "[console_scripts]\nbaz=bar:baz.boom.bim\nfoo=foo:bar\n" "fox=fuz.foo:bar.baz\n\n") metadata = """\ Metadata-Version: 2.1 Name: simple-project Version: 1.2.3 Summary: Some description. Home-page: https://python-poetry.org License: MIT Keywords: packaging,dependency,poetry Author: Sébastien Eustace Author-email: [email protected] Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Project-URL: Documentation, https://python-poetry.org/docs Project-URL: Repository, https://github.com/python-poetry/poetry Description-Content-Type: text/x-rst My Package ========== """ assert metadata == dist_info.joinpath("METADATA").read_text( encoding="utf-8") with open(dist_info.joinpath("RECORD"), encoding="utf-8", newline="") as f: reader = csv.reader(f) records = list(reader) assert all(len(row) == 3 for row in records) record_entries = {row[0] for row in records} pth_file = "simple_project.pth" assert tmp_venv.site_packages.exists(pth_file) assert str(tmp_venv.site_packages.find(pth_file)[0]) in record_entries assert str(tmp_venv._bin_dir.joinpath("foo")) in record_entries assert str(tmp_venv._bin_dir.joinpath("baz")) in record_entries assert str(dist_info.joinpath("METADATA")) in record_entries assert str(dist_info.joinpath("INSTALLER")) in record_entries assert str(dist_info.joinpath("entry_points.txt")) in record_entries assert str(dist_info.joinpath("RECORD")) in record_entries assert str(dist_info.joinpath("direct_url.json")) in record_entries baz_script = f"""\ #!{tmp_venv.python} import sys from bar import baz if __name__ == '__main__': sys.exit(baz.boom.bim()) """ assert baz_script == tmp_venv._bin_dir.joinpath("baz").read_text() foo_script = f"""\ #!{tmp_venv.python} import sys from foo import bar if __name__ == '__main__': sys.exit(bar()) """ assert foo_script == tmp_venv._bin_dir.joinpath("foo").read_text() fox_script = f"""\ #!{tmp_venv.python} import sys from fuz.foo import bar if __name__ == '__main__': sys.exit(bar.baz()) """ assert fox_script == tmp_venv._bin_dir.joinpath("fox").read_text()