Skip to content

Commit

Permalink
general: switch from flat layout to src layout
Browse files Browse the repository at this point in the history
this is long overdue and much friendlier to python tooling ecosystem

should be backwards compatible with existing editable installs, with a warning to reinstall properly

see #316
  • Loading branch information
karlicoss committed Jan 24, 2025
1 parent bb703c8 commit f8a55f7
Show file tree
Hide file tree
Showing 225 changed files with 99 additions and 34 deletions.
6 changes: 5 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import _pytest.pathlib

# we consider all dirs in repo/ to be namespace packages
root_dir = pathlib.Path(__file__).absolute().parent.resolve() # / 'src'
root_dir = pathlib.Path(__file__).absolute().parent.resolve() / 'src'
assert root_dir.exists(), root_dir

# TODO assert it contains package name?? maybe get it via setuptools..
Expand All @@ -21,6 +21,10 @@
# takes a full abs path to the test file and needs to return the path to the 'root' package on the filesystem
resolve_pkg_path_orig = _pytest.pathlib.resolve_package_path
def resolve_package_path(path: pathlib.Path) -> Optional[pathlib.Path]:
if 'tests_misc/conftest.py' in str(path):
# ugh. otherwise it can't resolve the path since tests_misc isn't under src/ dir..
return resolve_pkg_path_orig(path)

result = path # search from the test file upwards
for parent in result.parents:
if str(parent) in namespace_pkg_dirs:
Expand Down
6 changes: 3 additions & 3 deletions demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

def run() -> None:
# uses fixed paths; worth it for the sake of demonstration
# assumes we're in /tmp/my_demo now
# assumes we're in /tmp/my_demo now (set by the context manager in main)

# 1. clone [email protected]:karlicoss/my.git
copytree(
Expand Down Expand Up @@ -44,15 +44,15 @@ def run() -> None:

# 4. point my.config to the Hypothesis data
mycfg_root = abspath('my_repo')
init_file = Path(mycfg_root) / 'my/config.py'
init_file = Path(mycfg_root) / 'src/my/config.py'
init_file.write_text(init_file.read_text().replace(
'/path/to/hypothesis/data',
hypothesis_backups,
))
#

# 4. now we can use it!
os.chdir(my_repo)
os.chdir('my_repo/src')

check_call([python, '-c', '''
import my.hypothesis
Expand Down
1 change: 1 addition & 0 deletions my
9 changes: 0 additions & 9 deletions my/core/internal.py

This file was deleted.

8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@


def main() -> None:
pkg = 'my'
subpackages = find_namespace_packages('.', include=('my.*',))
pkgs = find_namespace_packages('src')
pkg = min(pkgs) # should be just 'my'

setup(
name='HPI', # NOTE: 'my' is taken for PyPi already, and makes discovering the project impossible. so we're using HPI
use_scm_version={
Expand All @@ -30,7 +31,8 @@ def main() -> None:

# eh. find_packages doesn't find anything
# find_namespace_packages can't find single file packages (like my/common.py)
packages=[pkg, *subpackages],
packages=pkgs,
package_dir={'': 'src'},
package_data={
pkg: [
# for mypy
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
21 changes: 13 additions & 8 deletions my/core/__init__.py → src/my/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@
LazyLogger = make_logger # TODO deprecate this in favor of make_logger


if not TYPE_CHECKING:
# we used to keep these here for brevity, but feels like it only adds confusion,
# e.g. suggest that we perhaps somehow modify builtin behaviour or whatever
# so best to prefer explicit behaviour
from dataclasses import dataclass
from pathlib import Path


__all__ = [
'__NOT_HPI_MODULE__',
'Json',
Expand Down Expand Up @@ -59,3 +51,16 @@
except:
pass
##


if not TYPE_CHECKING:
# we used to keep these here for brevity, but feels like it only adds confusion,
# e.g. suggest that we perhaps somehow modify builtin behaviour or whatever
# so best to prefer explicit behaviour
from dataclasses import dataclass
from pathlib import Path


from .internal import warn_if_not_using_src_layout

warn_if_not_using_src_layout(path=__path__)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
54 changes: 54 additions & 0 deletions src/my/core/internal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Utils specific to hpi core, shouldn't really be used by HPI modules
"""
from __future__ import annotations


def assert_subpackage(name: str) -> None:
# can lead to some unexpected issues if you 'import cachew' which being in my/core directory.. so let's protect against it
# NOTE: if we use overlay, name can be smth like my.origg.my.core.cachew ...
assert name == '__main__' or 'my.core' in name, f'Expected module __name__ ({name}) to be __main__ or start with my.core'


def _is_editable(package_name: str) -> bool:
import importlib.metadata

dist = importlib.metadata.distribution(package_name)
dist_files = dist.files or []
for path in dist_files:
if str(path).endswith('.pth'):
return True
return False


def warn_if_not_using_src_layout(path: list[str]) -> None:
contains_src = any('/src/my/' in p for p in path)
if contains_src:
return

# __package__ won't work because it's gonna be 'my' rather than 'hpi'
# seems like it's quite annoying to get distribition name from within the package, so easier to hardcode..
distribution_name = 'hpi'
try:
editable = _is_editable(distribution_name)
except:
# it would be annoying if this somehow fails during the very first HPI import...
# so just make defensive
return

if not editable:
# nothing to check
return


from . import warnings

MSG = '''
Seems that your HPI is installed as editable and uses flat layout ( my/ directory next to .git folder).
This was the case in older HPI versions (pre-20250123), but now src-layout is recommended ( src/my/ directory next to .git folder).
Reinstall your HPI as editable again via 'pip install --editable /path/to/hpi'.
See https://github.com/karlicoss/HPI/issues/316 for more info.
'''

warnings.high(MSG)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions my/tests/commits.py → src/my/tests/commits.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from my.coding.commits import commits
from my.core.cfg import tmp_config

from .common import hpi_repo_root


pytestmark = pytest.mark.skipif(
os.name == 'nt',
reason='TODO figure out how to install fd-find on Windows',
Expand All @@ -29,14 +32,11 @@ def prepare(tmp_path: Path):
# - bare repos
# - canonical name
# - caching?
hpi_repo_root = Path(__file__).absolute().parent.parent.parent
assert (hpi_repo_root / '.git').exists(), hpi_repo_root

class config:
class commits:
emails = {'[email protected]'}
names = {'Dima'}
roots = [hpi_repo_root]
roots = [hpi_repo_root()]

with tmp_config(modules='my.coding.commits', config=config):
yield
9 changes: 8 additions & 1 deletion my/tests/common.py → src/my/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@
)


def hpi_repo_root() -> Path:
root_dir = Path(__file__).absolute().parent.parent.parent.parent
src_dir = root_dir / 'src'
assert src_dir.exists(), src_dir
return root_dir


def testdata() -> Path:
d = Path(__file__).absolute().parent.parent.parent / 'testdata'
d = hpi_repo_root() / 'testdata'
assert d.exists(), d
return d

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ class via_location:
def _prepare_takeouts_dir(tmp_path: Path) -> Path:
from .common import testdata

testdata_dir = testdata()
try:
track = one(testdata().rglob('italy-slovenia-2017-07-29.json'))
track = one(testdata_dir.rglob('italy-slovenia-2017-07-29.json'))
except ValueError as e:
raise RuntimeError('testdata not found, setup git submodules?') from e
raise RuntimeError(f'testdata not found in {testdata_dir}, setup git submodules?') from e

# todo ugh. unnecessary zipping, but at the moment takeout provider doesn't support plain dirs
import zipfile
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ uv_seed = true # seems necessary so uv creates separate venvs per tox env?
deps =
-e .[testing]
commands =
{envpython} -m ruff check my/
{envpython} -m ruff check src/


# just the very core tests with minimal dependencies
Expand All @@ -46,7 +46,7 @@ commands =
# ignore orgmode because it imports orgparse
# tbh not sure if it even belongs to core, maybe move somewhere else..
# same with pandas?
--ignore my/core/orgmode.py \
--ignore src/my/core/orgmode.py \
{posargs}


Expand Down Expand Up @@ -154,7 +154,7 @@ commands =
{posargs}

{envpython} -m mypy --no-install-types \
tests
tests_misc

# note: this comment doesn't seem relevant anymore, but keeping it in case the issue happens again
# > ugh ... need to reset HOME, otherwise user's site-packages are somehow leaking into mypy's path...
Expand Down

0 comments on commit f8a55f7

Please sign in to comment.