Skip to content

Commit

Permalink
Add console_scripts field in result & reformat result structure (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
vhaldemar authored Apr 23, 2024
1 parent 9d883a4 commit 819ccae
Show file tree
Hide file tree
Showing 10 changed files with 1,622 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ dmypy.json
[._]sw[a-p]

poetry.lock
.python-version
4 changes: 3 additions & 1 deletion envzy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .auto import AutoExplorer
from .base import ModulePathsList, PackagesDict, BaseExplorer
from .base import BaseExplorer
from .spec import ModulePathsList, PackagesDict, EnvironmentSpec
from .exceptions import BadPypiIndex
from .pypi import PYPI_INDEX_URL_DEFAULT, validate_pypi_index_url

Expand All @@ -8,6 +9,7 @@
'BaseExplorer',
'ModulePathsList',
'PackagesDict',
'EnvironmentSpec',
'BadPypiIndex',
'PYPI_INDEX_URL_DEFAULT',
'validate_pypi_index_url',
Expand Down
43 changes: 36 additions & 7 deletions envzy/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import sys
from dataclasses import dataclass, field
from operator import attrgetter
from logging import getLogger
from typing import List, Type, TypeVar, Tuple, Union, Sequence

from .base import BaseExplorer, ModulePathsList, PackagesDict
from .base import BaseExplorer
from .classify import ModuleClassifier
from .search import VarsNamespace, get_transitive_namespace_dependencies
from .spec import ModulePathsList, PackagesDict, EnvironmentSpec
from .packages import (
BrokenModules,
LocalPackage,
Expand All @@ -33,8 +35,10 @@ class AutoExplorer(BaseExplorer):
search_stop_list: Sequence[str] = ()

def get_local_module_paths(self, namespace: VarsNamespace) -> ModulePathsList:
packages = self._get_packages(namespace, LocalPackage)
packages = self._get_packages(namespace)
return self._get_local_module_paths(self._filter(packages, LocalPackage))

def _get_local_module_paths(self, packages: List[LocalPackage]) -> ModulePathsList:
filtered: List[LocalPackage] = []
binary: List[LocalPackage] = []
nonbinary: List[LocalPackage] = []
Expand Down Expand Up @@ -87,11 +91,13 @@ def get_local_module_paths(self, namespace: VarsNamespace) -> ModulePathsList:
nonbinary
)

return list(set().union(*(p.paths for p in nonbinary)))
return sorted(set().union(*(p.paths for p in nonbinary)))

def get_pypi_packages(self, namespace: VarsNamespace) -> PackagesDict:
packages = self._get_packages(namespace, PypiDistribution)
packages = self._get_packages(namespace)
return self._get_pypi_packages(self._filter(packages, PypiDistribution))

def _get_pypi_packages(self, packages: List[PypiDistribution]) -> PackagesDict:
overrided: List[PypiDistribution] = []
bad_platform: List[PypiDistribution] = []
good: List[PypiDistribution] = []
Expand Down Expand Up @@ -129,11 +135,34 @@ def get_pypi_packages(self, namespace: VarsNamespace) -> PackagesDict:
**self.additional_pypi_packages
}

def get_environment_spec(self, namespace: VarsNamespace) -> EnvironmentSpec:
packages = self._get_packages(namespace)

local_packages = self._filter(packages, LocalPackage)
local_module_paths = self._get_local_module_paths(local_packages)
console_scripts = self._get_console_scripts(local_packages)

pypi_packages = self._get_pypi_packages(self._filter(packages, PypiDistribution))

return EnvironmentSpec(
packages=sorted(packages, key=attrgetter('name')),
local_module_paths=sorted(local_module_paths),
console_scripts=sorted(console_scripts),
pypi_packages=pypi_packages
)

def _get_console_scripts(self, packages: List[LocalPackage]) -> List[str]:
return list(
frozenset().union(*(p.console_scripts for p in packages))
)

def _filter(self, packages: List[BasePackage], filter_class: Type[P]) -> List[P]:
return [p for p in packages if isinstance(p, filter_class)]

def _get_packages(
self,
namespace: VarsNamespace,
filter_class: Type[P],
) -> List[P]:
) -> List[BasePackage]:
stop_list = frozenset(self.search_stop_list)
modules = get_transitive_namespace_dependencies(namespace, stop_list=stop_list)

Expand All @@ -151,4 +180,4 @@ def _get_packages(
'so these moduels will be omitted: %s', broken
)

return [p for p in packages if isinstance(p, filter_class)]
return list(packages)
11 changes: 6 additions & 5 deletions envzy/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from __future__ import annotations

from abc import abstractmethod
from typing import List, Dict
from typing_extensions import TypeAlias
from .search import VarsNamespace

ModulePathsList: TypeAlias = List[str]
PackagesDict: TypeAlias = Dict[str, str]
from .search import VarsNamespace
from .spec import ModulePathsList, PackagesDict, EnvironmentSpec


class BaseExplorer:
Expand All @@ -17,3 +14,7 @@ def get_local_module_paths(self, namespace: VarsNamespace) -> ModulePathsList:
@abstractmethod
def get_pypi_packages(self, namespace: VarsNamespace) -> PackagesDict:
raise NotImplementedError

@abstractmethod
def get_environment_spec(self, namespace: VarsNamespace) -> EnvironmentSpec:
raise NotImplementedError
22 changes: 18 additions & 4 deletions envzy/classify.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import json
import os
import site
import sys
from functools import lru_cache
from pathlib import Path
from typing import FrozenSet, Set, Dict, cast, Iterable, Tuple, Union, List, Optional
from types import ModuleType

Expand Down Expand Up @@ -60,6 +62,8 @@ def __init__(
self.names_to_distributions = get_names_to_distributions()
self.requirements_to_meta_packages = get_requirements_to_meta_packages()

self.scripts_directory = Path(sys.prefix).resolve() / 'bin'

self.bad_prefixes = frozenset([
site.getusersitepackages()
]) | set(site.getsitepackages())
Expand Down Expand Up @@ -106,6 +110,8 @@ def _classify_modules(
if not filename:
continue

filename = str(Path(filename).resolve())

# We also not interested in standard modules
if (
top_level in self.stdlib_module_names or
Expand Down Expand Up @@ -169,14 +175,15 @@ def _classify_distribution(
have_server_supported_tags=have_server_supported_tags,
)

paths, bad_paths = self._get_distribution_paths(distribution)
paths, console_scripts, bad_paths = self._get_distribution_paths(distribution)
is_binary = distribution in binary_distributions
return LocalDistribution(
name=distribution.name,
version=distribution.version,
paths=paths,
is_binary=is_binary,
bad_paths=bad_paths,
console_scripts=console_scripts,
)

def _classify_modules_without_distributions(
Expand Down Expand Up @@ -218,7 +225,8 @@ def _classify_modules_without_distributions(
package = LocalPackage(
name=top_level,
paths=frozenset(paths),
is_binary=top_level in binary_distributions
is_binary=top_level in binary_distributions,
console_scripts=frozenset(),
)
result.add(package)

Expand Down Expand Up @@ -394,19 +402,25 @@ def _check_distribution_platform_at_pypi(

def _get_distribution_paths(
self, distribution: Distribution
) -> Tuple[FrozenSet[str], FrozenSet[str]]:
) -> Tuple[FrozenSet[str], FrozenSet[str], FrozenSet[str]]:
"""
If Distribution files are foo/bar, foo/baz and foo1,
we want to return {<site-packages>/foo, <site-packages>/foo1}
"""

paths = set()
bad_paths = set()
console_scripts = set()

base_path = distribution.locate_file('').resolve()

for path in distribution.files or ():
abs_path = distribution.locate_file(path).resolve()

if self.scripts_directory in abs_path.parents:
console_scripts.add(str(abs_path))
continue

if base_path not in abs_path.parents:
bad_paths.add(str(abs_path))
continue
Expand All @@ -416,7 +430,7 @@ def _get_distribution_paths(
result_path = base_path / first_part
paths.add(str(result_path))

return frozenset(paths), frozenset(bad_paths)
return frozenset(paths), frozenset(console_scripts), frozenset(bad_paths)

def _check_module_is_binary(self, module: ModuleType) -> bool:
loader = getattr(module, '__loader__', None)
Expand Down
3 changes: 2 additions & 1 deletion envzy/packages.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import FrozenSet, Dict, Tuple
from typing import FrozenSet, Tuple


@dataclass(frozen=True)
Expand All @@ -12,6 +12,7 @@ class BasePackage:
@dataclass(frozen=True)
class LocalPackage(BasePackage):
paths: FrozenSet[str]
console_scripts: FrozenSet[str]
is_binary: bool


Expand Down
19 changes: 19 additions & 0 deletions envzy/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import List, Dict
from typing_extensions import TypeAlias

from .packages import BasePackage

ModulePathsList: TypeAlias = List[str]
PackagesDict: TypeAlias = Dict[str, str]
PackagesList: TypeAlias = List[BasePackage]


@dataclass
class EnvironmentSpec:
packages: PackagesList
local_module_paths: ModulePathsList
pypi_packages: PackagesDict
console_scripts: ModulePathsList
54 changes: 53 additions & 1 deletion tests/test_auto.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import sys
from envzy import AutoExplorer
import pytest
from envzy import AutoExplorer, EnvironmentSpec
from envzy.packages import PypiDistribution, LocalDistribution


def test_defaults(pypi_index_url):
Expand All @@ -14,3 +16,53 @@ def test_defaults(pypi_index_url):
assert explorer.pypi_index_url == pypi_index_url
assert explorer.target_python == sys.version_info[:2]
assert explorer.search_stop_list == ()


@pytest.mark.vcr
def test_get_environment_spec(pypi_index_url, site_packages, env_prefix):
import lzy_test_project

explorer = AutoExplorer()

spec = explorer.get_environment_spec({'foo': lzy_test_project})

assert spec == EnvironmentSpec(
packages=[
LocalDistribution(
name='lzy-test-project',
paths=frozenset({
f'{site_packages}/lzy_test_project',
f'{site_packages}/lzy_test_project-3.0.0.dist-info'
}),
is_binary=False,
version='3.0.0',
bad_paths=frozenset(),
console_scripts=frozenset({f'{env_prefix}/bin/lzy_test_project_bin'}),

),
PypiDistribution(
name='sampleproject',
version='3.0.0',
pypi_index_url='https://pypi.org/simple/',
have_server_supported_tags=True
),
],
local_module_paths=[
f'{site_packages}/lzy_test_project',
f'{site_packages}/lzy_test_project-3.0.0.dist-info'
],
pypi_packages={
'sampleproject': '3.0.0'
},
console_scripts=[
f'{env_prefix}/bin/lzy_test_project_bin'
]
)

assert explorer.get_local_module_paths({'foo': lzy_test_project}) == [
f'{site_packages}/lzy_test_project',
f'{site_packages}/lzy_test_project-3.0.0.dist-info'
]
assert explorer.get_pypi_packages({'foo': lzy_test_project}) == {
'sampleproject': '3.0.0'
}
18 changes: 12 additions & 6 deletions tests/test_classify.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def test_classify_local_packages(
assert classifier.classify([level1]) == frozenset([LocalPackage(
name='modules_for_tests',
paths=frozenset([str(get_test_data_path('modules_for_tests'))]),
is_binary=False
is_binary=False,
console_scripts=frozenset(),
)])

# two modules within one namespace but different locations
Expand All @@ -42,14 +43,16 @@ def test_classify_local_packages(
str(get_test_data_path('modules_for_tests')),
str(get_test_data_path('namespace', 'modules_for_tests')),
]),
is_binary=False
is_binary=False,
console_scripts=frozenset(),
)])

# toplevel module without a package
assert classifier.classify([empty_module]) == frozenset([LocalPackage(
name='empty_module',
paths=frozenset([str(get_test_data_path('empty_module.py'))]),
is_binary=False
is_binary=False,
console_scripts=frozenset(),
)])


Expand Down Expand Up @@ -77,7 +80,8 @@ def test_classify_pypi_packages(
}),
is_binary=False,
version='3.0.0',
bad_paths=frozenset({f'{env_prefix}/bin/lzy_test_project_bin'})
bad_paths=frozenset(),
console_scripts=frozenset({f'{env_prefix}/bin/lzy_test_project_bin'}),
),
})

Expand Down Expand Up @@ -130,7 +134,8 @@ def test_classify_local_distribution(
}),
is_binary=False,
version='3.0.0',
bad_paths=frozenset({f'{env_prefix}/bin/lzy_test_project_bin'})
bad_paths=frozenset(),
console_scripts=frozenset({f'{env_prefix}/bin/lzy_test_project_bin'}),
)

def classify(module) -> Set[BasePackage]:
Expand Down Expand Up @@ -174,7 +179,8 @@ def test_classify_editable_distribution(classifier: ModuleClassifier, get_test_d
paths=frozenset({
f'{get_test_data_path()}/lzy_test_project_editable/src/lzy_test_project_editable'
}),
is_binary=False
is_binary=False,
console_scripts=frozenset(),
)
})

Expand Down
Loading

0 comments on commit 819ccae

Please sign in to comment.