Skip to content

Commit

Permalink
Merge branch 'master' into 1423-better-pyapp-integration
Browse files Browse the repository at this point in the history
  • Loading branch information
johannesloibl authored Jun 24, 2024
2 parents 5dcbd86 + 72e0942 commit 96629d9
Show file tree
Hide file tree
Showing 21 changed files with 275 additions and 37 deletions.
2 changes: 1 addition & 1 deletion backend/src/hatchling/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.24.2'
__version__ = '1.25.0'
58 changes: 58 additions & 0 deletions backend/src/hatchling/builders/macos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

import os
import platform
import re

__all__ = ['process_macos_plat_tag']


def process_macos_plat_tag(plat: str, /, *, compat: bool) -> str:
"""
Process the macOS platform tag. This will normalize the macOS version to
10.16 if compat=True. If the MACOSX_DEPLOYMENT_TARGET environment variable
is set, then it will be used instead for the target version. If archflags
is set, then the archs will be respected, including a universal build.
"""
# Default to a native build
current_arch = platform.machine()
arm = current_arch == 'arm64'

# Look for cross-compiles
archflags = os.environ.get('ARCHFLAGS', '')
if archflags and (archs := re.findall(r'-arch (\S+)', archflags)):
new_arch = 'universal2' if set(archs) == {'x86_64', 'arm64'} else archs[0]
arm = archs == ['arm64']
plat = f'{plat[: plat.rfind(current_arch)]}{new_arch}'

# Process macOS version
if sdk_match := re.search(r'macosx_(\d+_\d+)', plat):
macos_version = sdk_match.group(1)
target = os.environ.get('MACOSX_DEPLOYMENT_TARGET', None)

try:
new_version = normalize_macos_version(target or macos_version, arm=arm, compat=compat)
except ValueError:
new_version = normalize_macos_version(macos_version, arm=arm, compat=compat)

return plat.replace(macos_version, new_version, 1)

return plat


def normalize_macos_version(version: str, *, arm: bool, compat: bool) -> str:
"""
Set minor version to 0 if major is 11+. Enforces 11+ if arm=True. 11+ is
converted to 10.16 if compat=True. Version is always returned in
"major_minor" format.
"""
version = version.replace('.', '_')
if '_' not in version:
version = f'{version}_0'
major, minor = (int(d) for d in version.split('_')[:2])
major = max(major, 11) if arm else major
minor = 0 if major >= 11 else minor # noqa: PLR2004
if compat and major >= 11: # noqa: PLR2004
major = 10
minor = 16
return f'{major}_{minor}'
24 changes: 3 additions & 21 deletions backend/src/hatchling/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,28 +783,10 @@ def get_best_matching_tag(self) -> str:
tag = next(iter(t for t in sys_tags() if 'manylinux' not in t.platform and 'musllinux' not in t.platform))
tag_parts = [tag.interpreter, tag.abi, tag.platform]

archflags = os.environ.get('ARCHFLAGS', '')
if sys.platform == 'darwin':
if archflags and sys.version_info[:2] >= (3, 8):
import platform
import re

archs = re.findall(r'-arch (\S+)', archflags)
if archs:
plat = tag_parts[2]
current_arch = platform.mac_ver()[2]
new_arch = 'universal2' if set(archs) == {'x86_64', 'arm64'} else archs[0]
tag_parts[2] = f'{plat[: plat.rfind(current_arch)]}{new_arch}'

if self.config.macos_max_compat:
import re

plat = tag_parts[2]
sdk_match = re.search(r'macosx_(\d+_\d+)', plat)
if sdk_match:
sdk_version_part = sdk_match.group(1)
if tuple(map(int, sdk_version_part.split('_'))) >= (11, 0):
tag_parts[2] = plat.replace(sdk_version_part, '10_16', 1)
from hatchling.builders.macos import process_macos_plat_tag

tag_parts[2] = process_macos_plat_tag(tag_parts[2], compat=self.config.macos_max_compat)

return '-'.join(tag_parts)

Expand Down
2 changes: 1 addition & 1 deletion backend/src/hatchling/cli/dep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def synced_impl(*, dependencies: list[str], python: str) -> None:

from packaging.requirements import Requirement

from hatchling.dep.core import dependencies_in_sync
from hatchling.cli.dep.core import dependencies_in_sync

sys_path = None
if python:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def dependencies_in_sync(
if sys_path is None:
sys_path = sys.path
if environment is None:
environment = default_environment()
environment = default_environment() # type: ignore

installed_distributions = DistributionCache(sys_path)
return all(dependency_in_sync(requirement, environment, installed_distributions) for requirement in requirements)
return all(dependency_in_sync(requirement, environment, installed_distributions) for requirement in requirements) # type: ignore
2 changes: 1 addition & 1 deletion docs/config/environment/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ features = [

### Dev mode

By default, environments will always reflect the current state of your project on disk. Set `dev-mode` to `false` to disable this behavior:
By default, environments will always reflect the current state of your project on disk, for example, by installing it in editable mode in a Python environment. Set `dev-mode` to `false` to disable this behavior and have your project installed only upon creation of a new environment. From then on, you need to manage your project installation manually.

```toml config-example
[tool.hatch.envs.static]
Expand Down
11 changes: 11 additions & 0 deletions docs/history/hatchling.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

## [1.25.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.25.0) - 2024-06-22 ## {: #hatchling-v1.25.0 }

***Changed:***

- The `macos-max-compat` option for the `wheel` target is now disabled by default and will be removed in a future release

***Added:***

- Artifacts for the `wheel` and `sdist` targets now have their permission bits normalized

***Fixed:***

- Ignore `manylinux`/`musllinux` tags for the `wheel` target artifact name when enabling the `infer_tag` build data
- The `wheel` target build data `infer_tag` when enabled now respects the `MACOSX_DEPLOYMENT_TARGET` environment variable

## [1.24.2](https://github.com/pypa/hatch/releases/tag/hatchling-v1.24.2) - 2024-04-22 ## {: #hatchling-v1.24.2 }

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ source_pkgs = ["hatch", "hatchling", "tests"]
omit = [
"backend/src/hatchling/__main__.py",
"backend/src/hatchling/bridge/*",
"backend/src/hatchling/cli/dep/*",
"backend/src/hatchling/ouroboros.py",
"src/hatch/__main__.py",
"src/hatch/cli/new/migrate.py",
Expand Down
2 changes: 1 addition & 1 deletion src/hatch/cli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ def ensure_plugin_dependencies(self, dependencies: list[Requirement], *, wait_me
if not dependencies:
return

from hatch.dep.sync import dependencies_in_sync
from hatch.env.utils import add_verbosity_flag
from hatchling.dep.core import dependencies_in_sync

if app_path := os.environ.get('PYAPP'):
from hatch.utils.env import PythonInfo
Expand Down
2 changes: 1 addition & 1 deletion src/hatch/cli/project/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def metadata(app, field):
"""
import json

from hatchling.dep.core import dependencies_in_sync
from hatch.dep.sync import dependencies_in_sync

if dependencies_in_sync(app.project.metadata.build.requires_complex):
from hatchling.metadata.utils import resolve_metadata_fields
Expand Down
2 changes: 1 addition & 1 deletion src/hatch/cli/version/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def version(app: Application, desired_version: str | None):
app.display(app.project.metadata.config['project']['version'])
return

from hatchling.dep.core import dependencies_in_sync
from hatch.dep.sync import dependencies_in_sync

with app.project.location.as_cwd():
if not (
Expand Down
File renamed without changes.
132 changes: 132 additions & 0 deletions src/hatch/dep/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from __future__ import annotations

import re
import sys
from importlib.metadata import Distribution, DistributionFinder

from packaging.markers import default_environment
from packaging.requirements import Requirement


class DistributionCache:
def __init__(self, sys_path: list[str]) -> None:
self._resolver = Distribution.discover(context=DistributionFinder.Context(path=sys_path))
self._distributions: dict[str, Distribution] = {}
self._search_exhausted = False
self._canonical_regex = re.compile(r'[-_.]+')

def __getitem__(self, item: str) -> Distribution | None:
item = self._canonical_regex.sub('-', item).lower()
possible_distribution = self._distributions.get(item)
if possible_distribution is not None:
return possible_distribution

# Be safe even though the code as-is will never reach this since
# the first unknown distribution will fail fast
if self._search_exhausted: # no cov
return None

for distribution in self._resolver:
name = distribution.metadata['Name']
if name is None:
continue

name = self._canonical_regex.sub('-', name).lower()
self._distributions[name] = distribution
if name == item:
return distribution

self._search_exhausted = True

return None


def dependency_in_sync(
requirement: Requirement, environment: dict[str, str], installed_distributions: DistributionCache
) -> bool:
if requirement.marker and not requirement.marker.evaluate(environment):
return True

distribution = installed_distributions[requirement.name]
if distribution is None:
return False

extras = requirement.extras
if extras:
transitive_requirements: list[str] = distribution.metadata.get_all('Requires-Dist', [])
if not transitive_requirements:
return False

available_extras: list[str] = distribution.metadata.get_all('Provides-Extra', [])

for requirement_string in transitive_requirements:
transitive_requirement = Requirement(requirement_string)
if not transitive_requirement.marker:
continue

for extra in extras:
# FIXME: This may cause a build to never be ready if newer versions do not provide the desired
# extra and it's just a user error/typo. See: https://github.com/pypa/pip/issues/7122
if extra not in available_extras:
return False

extra_environment = dict(environment)
extra_environment['extra'] = extra
if not dependency_in_sync(transitive_requirement, extra_environment, installed_distributions):
return False

if requirement.specifier and not requirement.specifier.contains(distribution.version):
return False

# TODO: handle https://discuss.python.org/t/11938
if requirement.url:
direct_url_file = distribution.read_text('direct_url.json')
if direct_url_file is not None:
import json

# https://packaging.python.org/specifications/direct-url/
direct_url_data = json.loads(direct_url_file)
if 'vcs_info' in direct_url_data:
url = direct_url_data['url']
vcs_info = direct_url_data['vcs_info']
vcs = vcs_info['vcs']
commit_id = vcs_info['commit_id']
requested_revision = vcs_info.get('requested_revision')

# Try a few variations, see https://peps.python.org/pep-0440/#direct-references
if (
requested_revision and requirement.url == f'{vcs}+{url}@{requested_revision}#{commit_id}'
) or requirement.url == f'{vcs}+{url}@{commit_id}':
return True

if requirement.url in {f'{vcs}+{url}', f'{vcs}+{url}@{requested_revision}'}:
import subprocess

if vcs == 'git':
vcs_cmd = [vcs, 'ls-remote', url]
if requested_revision:
vcs_cmd.append(requested_revision)
# TODO: add elifs for hg, svn, and bzr https://github.com/pypa/hatch/issues/760
else:
return False
result = subprocess.run(vcs_cmd, capture_output=True, text=True) # noqa: PLW1510
if result.returncode or not result.stdout.strip():
return False
latest_commit_id, *_ = result.stdout.split()
return commit_id == latest_commit_id

return False

return True


def dependencies_in_sync(
requirements: list[Requirement], sys_path: list[str] | None = None, environment: dict[str, str] | None = None
) -> bool:
if sys_path is None:
sys_path = sys.path
if environment is None:
environment = default_environment() # type: ignore

installed_distributions = DistributionCache(sys_path)
return all(dependency_in_sync(requirement, environment, installed_distributions) for requirement in requirements) # type: ignore
2 changes: 1 addition & 1 deletion src/hatch/env/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def dependencies_in_sync(self):
if not self.dependencies:
return True

from hatchling.dep.core import dependencies_in_sync
from hatch.dep.sync import dependencies_in_sync

return dependencies_in_sync(
self.dependencies_complex, sys_path=self.python_info.sys_path, environment=self.python_info.environment
Expand Down
4 changes: 2 additions & 2 deletions src/hatch/env/virtual.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def dependencies_in_sync(self):
if not self.dependencies:
return True

from hatchling.dep.core import dependencies_in_sync
from hatch.dep.sync import dependencies_in_sync

with self.safe_activation():
return dependencies_in_sync(
Expand All @@ -199,7 +199,7 @@ def sync_dependencies(self):
def build_environment(self, dependencies):
from packaging.requirements import Requirement

from hatchling.dep.core import dependencies_in_sync
from hatch.dep.sync import dependencies_in_sync

if not self.build_environment_exists():
with self.expose_uv():
Expand Down
2 changes: 1 addition & 1 deletion src/hatch/utils/dep.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def hash_dependencies(requirements: list[Requirement]) -> str:
def get_project_dependencies_complex(
environment: EnvironmentInterface,
) -> tuple[dict[str, Requirement], dict[str, dict[str, Requirement]]]:
from hatchling.dep.core import dependencies_in_sync
from hatch.dep.sync import dependencies_in_sync

dependencies_complex = {}
optional_dependencies_complex = {}
Expand Down
Loading

0 comments on commit 96629d9

Please sign in to comment.