diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4698199..d0d64a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,24 +1,16 @@ name: Test - -on: - push: - branches: - - main - pull_request: - branches: - - main - +on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.gitignore b/.gitignore index 6f6712f..e9e53c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ *.pyc -.tox/ +/.tox .coverage __pycache__ -castaway.egg-info/ -venv/ -htmlcov/ -temp/ +/src/castaway.egg-info +/venv +/htmlcov +/temp +/dist +/.pytest_cache .python-version diff --git a/LICENSE b/LICENSE index 9efdc26..23f9062 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 David A Krauth +Copyright (c) 2025 David A Krauth Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1cdc717 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "castaway" +version = "1.1.0" +description = "Simple wrapper for dotenv, with casting" +requires-python = ">=3.10" +readme = { file = "README.rst", content-type = "text/x-rst" } +license = { file = "LICENSE" } +authors = [ + { name = "David Krauth", email = "dakrauth@gmail.com"}, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Environment :: Console", + "Framework :: Django", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = ["python-dotenv[cli]==1.0.1"] + +[project.urls] +Homepage = "http://github.com/dakrauth/castaway" + +[project.optional-dependencies] +django = [ "dj-email-url==1.0.6", "dj-database-url==2.3.0" ] +test = ["pytest-cov", "tox"] + +[tool.setuptools] +package-dir = { "" = "src" } +packages = [ "castaway" ] + +[tool.ruff] +line-length = 100 +indent-width = 4 + +[tool.ruff.lint] +ignore = ["E741"] diff --git a/run b/run new file mode 100755 index 0000000..12b2b5d --- /dev/null +++ b/run @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail +TIMEFORMAT="Task completed in %3lR" +BIN="./venv/bin" + +function help { + echo "Options:" + echo " init: Setup dev environment" + echo " lint: Linter" + echo " check: Formatting checks" + echo " format: Run formatter" + echo " test: Execute tests" + echo " coverage: Run tests with code coverage" + echo " clean: Remove dev, test, and build artifacts" +} + +function init { + python3 -m venv --prompt castaway venv + "${BIN}/python" -m pip install -e ".[django,test]" +} + +function lint { + "${BIN}/ruff" check castaway.py tests +} + +function check { + "${BIN}/ruff" format --check --diff src/castaway tests +} + +function format { + "${BIN}/ruff" format src/castaway tests +} + +function test { + "${BIN}/pytest" -s -Werror tests "$@" +} + +function clean { + rm -rf .ruff_cache .tox htmlcov .coverage dist pytest_cache src/castaway.egg-info +} + +function coverage { + "${BIN}/pytest" \ + --cov-report html \ + --cov-report term \ + --cov=castaway +} + +if [ -z "$1" ]; then + help +else + what=$1 + shift + time ${what:-help} "$@" +fi diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 69e0eab..0000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[metadata] -license_file = LICENSE - -[flake8] -max-line-length = 100 - -[coverage:run] -branch = True -source = castaway.py diff --git a/setup.py b/setup.py deleted file mode 100644 index f6a5050..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import setup - -django_extras = ["dj-email-url==1.0.6", "dj-database-url==2.1.0"] -test_extras = ["pytest-cov"] -with open("README.rst", "r") as f: - long_description = f.read() - - -setup( - name="castaway", - version="1.0.0", - description="Simple wrapper for dotenv, with casting", - long_description=long_description, - long_description_content_type="text/x-rst", - url="http://github.com/dakrauth/castaway", - author="David Krauth", - author_email="dakrauth@gmail.com", - license="MIT", - zip_safe=False, - platforms=["any"], - py_modules=["castaway"], - python_requires=">=3.8", - install_requires=["python-dotenv[cli]==1.0.0"], - extras_require={ - "django": django_extras, - "test": test_extras, - "all": django_extras + test_extras, - }, - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Web Environment", - "Framework :: Django", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - ], -) diff --git a/castaway.py b/src/castaway/__init__.py similarity index 64% rename from castaway.py rename to src/castaway/__init__.py index 393de02..2dad27e 100644 --- a/castaway.py +++ b/src/castaway/__init__.py @@ -1,15 +1,12 @@ import os +from pathlib import Path import dotenv required = object() def cast_bool(val): - return ( - val.lower() in {"1", "yes", "true", "y", "on"} - if isinstance(val, str) - else bool(val) - ) + return val.lower() in {"1", "yes", "true", "y", "on"} if isinstance(val, str) else bool(val) def cast_list(val): @@ -30,8 +27,11 @@ def cast_django_email(val): class Config: def __init__(self, filename=".env", **castings): - self.filename = filename - self.found_path = None + if isinstance(filename, (str, Path)): + filename = [filename] + + self.filename = [str(f) for f in filename] + self.found_path = [] self.castings = { bool: cast_bool, @@ -40,12 +40,15 @@ def __init__(self, filename=".env", **castings): "django_email": cast_django_email, } self.castings.update(**castings) - - if os.path.exists(self.filename): - self.found_path = self.filename - else: - self.found_path = dotenv.find_dotenv(self.filename, usecwd=True) - self.values = dotenv.dotenv_values(self.found_path, verbose=True) + self.values = {} + for path in self.filename: + if os.path.exists(path): + found = path + else: + found = dotenv.find_dotenv(path, usecwd=True) + + self.found_path.append(found) + self.values.update(**dotenv.dotenv_values(found, verbose=True)) def add_castings(self, **kwargs): self.castings.update(kwargs) diff --git a/tests/.env2 b/tests/.env2 new file mode 100644 index 0000000..f936ef8 --- /dev/null +++ b/tests/.env2 @@ -0,0 +1 @@ +CASTAWAY_DECIMAL=3.14159 diff --git a/tests/test_castenv.py b/tests/test_castenv.py index f668569..0409685 100644 --- a/tests/test_castenv.py +++ b/tests/test_castenv.py @@ -1,4 +1,5 @@ import os +import decimal import pytest import castaway @@ -13,12 +14,14 @@ def set_cwd(): @pytest.fixture def cfg(set_cwd): - return castaway.Config() + return castaway.Config(filename=[".env", ".env2"]) def test_default_config(set_cwd): config = castaway.config + config.add_castings(decimal=decimal.Decimal) assert config("CASTAWAY_INT", cast=int) == 23 + assert config("CASTAWAY_DECIMAL", cast="decimal") == decimal.Decimal("2.3") def test_bool(cfg): @@ -39,10 +42,8 @@ def test_list(cfg): def test_custom_cast(cfg): - import decimal - cfg.add_castings(decimal=decimal.Decimal) - assert cfg("CASTAWAY_DECIMAL", cast="decimal") == decimal.Decimal("2.3") + assert cfg("CASTAWAY_DECIMAL", cast="decimal") == decimal.Decimal("3.14159") def test_override(): @@ -61,7 +62,9 @@ def test_required_failure(cfg): def test_dj_database(cfg): - assert cfg("CASTAWAY_DJ_DATABASE", cast="django_db") == { + result = cfg("CASTAWAY_DJ_DATABASE", cast="django_db") + result.pop("DISABLE_SERVER_SIDE_CURSORS", None) + assert result == { "ENGINE": "django.db.backends.mysql", "OPTIONS": {"init_command": "SET storage_engine=InnoDB", "charset": "utf8mb4"}, "NAME": "dbname", diff --git a/tox.ini b/tox.ini index 46573eb..1bb9671 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] skipsdist = True envlist = - py{38,39,310,311,312} + py{310,311,312,313} [testenv] usedevelop = True extras = django commands = - pip install -e .[all] + pip install -e .[django,test] pytest deps = pytest-cov @@ -33,22 +33,13 @@ commands = find {toxinidir} -type d -name "__pycache__" -delete rm -f {toxworkdir} {toxinidir}/.pytest_cache {toxinidir}/build {toxinidir}/dist -[testenv:pep8] -description = Run PEP8 flake8 -skipsdist = true -skip_install = true -basepython = python3.10 -deps = flake8 -commands = flake8 castaway.py tests - [pytest] testpaths = tests [gh-actions] python = - 3.8: py38 - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313