Skip to content

Commit

Permalink
make overrides system simpler, remove caching for now
Browse files Browse the repository at this point in the history
  • Loading branch information
pirate committed Oct 11, 2024
1 parent a9a143e commit 293bf47
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 384 deletions.
27 changes: 18 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ curl.exec(['--version']) # curl 7.81.0 (x86_64-pc-linux-gnu) libcur

### Example: Finding/Installing django with pip (w/ customized binpath resolution behavior)
pip = PipProvider(
abspath_handler={'*': lambda bin_name, **context: inspect.getfile(bin_name)}, # use python inspect to get path instead of os.which
abspath_handler={'*': lambda self, bin_name, **context: inspect.getfile(bin_name)}, # use python inspect to get path instead of os.which
)
django_bin = pip.load_or_install('django') # Binary('django', provider=pip)
print(django_bin.abspath) # Path('/usr/lib/python3.10/site-packages/django/__init__.py')
Expand All @@ -182,20 +182,29 @@ It can define one or more `BinProvider`s that it supports, along with overrides
```python
from pydantic_pkgr import BinProvider, Binary, BinProviderName, BinName, ProviderLookupDict, SemVer

class CustomBrewProvider(BrewProvider):
name: str = 'custom_brew'

def get_macos_packages(self, bin_name: str, **context) -> List[str]:
extra_packages_lookup_table = json.load(Path('macos_packages.json'))
return extra_packages_lookup_table.get(platform.machine(), [bin_name])


### Example: Create a re-usable class defining a binary and its providers
class YtdlpBinary(Binary):
name: BinName = 'ytdlp'
description: str = 'YT-DLP (Replacement for YouTube-DL) Media Downloader'

binproviders_supported: List[BinProvider] = [EnvProvider(), PipProvider(), AptProvider(), BrewProvider()]
binproviders_supported: List[BinProvider] = [EnvProvider(), PipProvider(), AptProvider(), CustomBrewProvider()]

# customize installed package names for specific package managers
provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
'pip': {'packages': lambda: ['yt-dlp[default,curl-cffi]']}},
'apt': {'packages': lambda: ['yt-dlp', 'ffmpeg']}},
'brew': {'packages': 'some.other.module.get_brew_packages'}}, # also accepts dotted import path to function
'pip': {'packages': ['yt-dlp[default,curl-cffi]']}, # can use literal values (packages -> List[str], version -> SemVer, abspath -> Path, install -> str log)
'apt': {'packages': lambda: ['yt-dlp', 'ffmpeg']}, # also accepts any pure Callable that returns a list of packages
'brew': {'packages': 'self.get_macos_packages'}, # also accepts string reference to function on self (where self is the BinProvider)
}


ytdlp = YtdlpBinary().load_or_install()
print(ytdlp.binprovider) # BrewProvider(...)
print(ytdlp.abspath) # Path('/opt/homebrew/bin/yt-dlp')
Expand All @@ -221,10 +230,10 @@ class DockerBinary(Binary):
},
'apt': {
# example: vary installed package name based on your CPU architecture
'packages': lambda: {
'amd64': 'docker',
'armv7l': 'docker-ce',
'arm64': 'docker-ce',
'packages': {
'amd64': ['docker'],
'armv7l': ['docker-ce'],
'arm64': ['docker-ce'],
}.get(platform.machine(), 'docker'),
},
}
Expand Down
20 changes: 14 additions & 6 deletions pydantic_pkgr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
HostExistsPath,
BinDirPath,
BinProviderName,
ProviderLookupDict,
ProviderHandlerRef,
ProviderHandler,
BinProviderOverrides,
BinaryOverrides,
ProviderFuncReturnValue,
HandlerType,
HandlerValue,
HandlerDict,
HandlerReturnValue,
bin_name,
bin_abspath,
bin_abspaths,
Expand Down Expand Up @@ -63,10 +66,15 @@
"HostBinPath",
"HostExistsPath",
"BinProviderName",
"ProviderLookupDict",
"ProviderHandlerRef",
"ProviderHandler",

# Override types
"BinProviderOverrides",
"BinaryOverrides",
"ProviderFuncReturnValue",
"HandlerType",
"HandlerValue",
"HandlerDict",
"HandlerReturnValue",

# Validator Functions
"bin_version",
Expand Down
84 changes: 71 additions & 13 deletions pydantic_pkgr/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
import shutil

from pathlib import Path
from typing import List, Dict, Callable, Literal, Any, Annotated
from typing import List, Tuple, Dict, Callable, Literal, Any, Annotated, Protocol, TYPE_CHECKING, TypedDict, NamedTuple
from typing_extensions import runtime_checkable

from pydantic import TypeAdapter, AfterValidator, BeforeValidator, ValidationError, validate_call

if TYPE_CHECKING:
from .binprovider import BinProvider


def validate_binprovider_name(name: str) -> str:
assert 1 < len(name) < 16, 'BinProvider names must be between 1 and 16 characters long'
assert name.replace('_', '').isalnum(), 'BinProvider names can only contain a-Z0-9 and underscores'
Expand Down Expand Up @@ -177,31 +182,84 @@ def bin_abspaths(bin_path_or_name: BinName | Path, PATH: PATHStr | None=None) ->

################## Types ##############################################

UNKNOWN_SHA256 = 'unknown'

def is_valid_sha256(sha256: str) -> str:
if sha256 == 'unknown':
if sha256 == UNKNOWN_SHA256:
return sha256
assert len(sha256) == 64
assert sha256.isalnum()
return sha256

Sha256 = Annotated[str, AfterValidator(is_valid_sha256)]

def is_valid_install_args(install_args: List[str]) -> List[str]:
def is_valid_install_args(install_args: List[str] | Tuple[str, ...] | str) -> Tuple[str, ...]:
"""Make sure a string is a valid install string for a package manager, e.g. ['yt-dlp', 'ffmpeg']"""
if isinstance(install_args, str):
install_args = [install_args]
assert install_args
assert all(len(arg) for arg in install_args)
return install_args
return tuple(install_args)

def is_name_of_method_on_self(method_name: str) -> str:
assert method_name.startswith('self.') and method_name.replace('.', '').replace('_', '').isalnum()
return method_name

InstallArgs = Annotated[Tuple[str, ...], AfterValidator(is_valid_install_args)]

SelfMethodName = Annotated[str, AfterValidator(is_name_of_method_on_self)]

def is_valid_python_dotted_import(import_str: str) -> str:
assert import_str and import_str.replace('.', '').replace('_', '').isalnum()
return import_str

InstallArgs = Annotated[List[str], AfterValidator(is_valid_install_args)]
# OVERRIDES Handler types

LazyImportStr = Annotated[str, AfterValidator(is_valid_python_dotted_import)]
AbspathFuncReturnValue = str | HostBinPath | None
VersionFuncReturnValue = str | Tuple[int, ...] | Tuple[str, ...] | NamedTuple | None # SemVer is a subclass of NamedTuple
PackagesFuncReturnValue = List[str] | Tuple[str, ...] | str | InstallArgs | None
InstallFuncReturnValue = str | None
ProviderFuncReturnValue = AbspathFuncReturnValue | VersionFuncReturnValue | PackagesFuncReturnValue | InstallFuncReturnValue

@runtime_checkable
class AbspathFuncWithArgs(Protocol):
def __call__(_self, binprovider: 'BinProvider', bin_name: BinName, **context) -> 'AbspathFuncReturnValue':
...

@runtime_checkable
class VersionFuncWithArgs(Protocol):
def __call__(_self, binprovider: 'BinProvider', bin_name: BinName, **context) -> 'VersionFuncReturnValue':
...

@runtime_checkable
class PackagesFuncWithArgs(Protocol):
def __call__(_self, binprovider: 'BinProvider', bin_name: BinName, **context) -> 'PackagesFuncReturnValue':
...

@runtime_checkable
class InstallFuncWithArgs(Protocol):
def __call__(_self, binprovider: 'BinProvider', bin_name: BinName, **context) -> 'InstallFuncReturnValue':
...

AbspathFuncWithNoArgs = Callable[[], AbspathFuncReturnValue]
VersionFuncWithNoArgs = Callable[[], VersionFuncReturnValue]
PackagesFuncWithNoArgs = Callable[[], PackagesFuncReturnValue]
InstallFuncWithNoArgs = Callable[[], InstallFuncReturnValue]

AbspathHandlerValue = SelfMethodName | AbspathFuncWithNoArgs | AbspathFuncWithArgs | AbspathFuncReturnValue
VersionHandlerValue = SelfMethodName | VersionFuncWithNoArgs | VersionFuncWithArgs | VersionFuncReturnValue
PackagesHandlerValue = SelfMethodName | PackagesFuncWithNoArgs | PackagesFuncWithArgs | PackagesFuncReturnValue
InstallHandlerValue = SelfMethodName | InstallFuncWithNoArgs | InstallFuncWithArgs | InstallFuncReturnValue

ProviderHandler = Callable[..., Any] | Callable[[], Any] # must take no args [], or [bin_name: str, **kwargs]
#ProviderHandlerStr = Annotated[str, AfterValidator(lambda s: s.startswith('self.'))]
ProviderHandlerRef = LazyImportStr | ProviderHandler
ProviderLookupDict = Dict[str, ProviderHandlerRef]
HandlerType = Literal['abspath', 'version', 'packages', 'install']
HandlerValue = AbspathHandlerValue | VersionHandlerValue | PackagesHandlerValue | InstallHandlerValue
HandlerReturnValue = AbspathFuncReturnValue | VersionFuncReturnValue | PackagesFuncReturnValue | InstallFuncReturnValue

class HandlerDict(TypedDict, total=False):
abspath: AbspathHandlerValue
version: VersionHandlerValue
packages: PackagesHandlerValue
install: InstallHandlerValue

# Binary.overrides map BinProviderName:HandlerType:Handler {'brew': {'packages': [...]}}
BinaryOverrides = Dict[BinProviderName, HandlerDict]

# BinProvider.overrides map BinName:HandlerType:Handler {'wget': {'packages': [...]}}
BinProviderOverrides = Dict[BinName | Literal['*'], HandlerDict]
Loading

0 comments on commit 293bf47

Please sign in to comment.