From cc6d0d6370945cddab613c24286e6a92905554eb Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Sat, 20 Apr 2024 13:38:24 -0500 Subject: [PATCH] Add YAML configuration file support The sample YAML config files do not have %(here)s, or any other interpolation. (The plaster-yaml interpolation code is broken, and, in any case, should probably be removed and replaced with template substitution.) The %(here)s are removed from testing.ini, in favor of a relative path. At the time of this writing the plaster-yaml package must be the HEAD of the "main" branch. It contains a bug fix that has not been released. The plan is to not submit these changes as a PR until there is a new plaster-yaml release. --- CHANGES.txt | 2 + README.rst | 2 + cookiecutter.json | 1 + hooks/post_gen_project.py | 34 +++++- tests/test_it.py | 110 ++++++++++++------ {{cookiecutter.repo_name}}/README.md | 11 +- {{cookiecutter.repo_name}}/development.ini | 36 ++++-- {{cookiecutter.repo_name}}/development.yaml | 94 +++++++++++++++ {{cookiecutter.repo_name}}/production.ini | 37 ++++-- {{cookiecutter.repo_name}}/production.yaml | 81 +++++++++++++ {{cookiecutter.repo_name}}/pyproject.toml | 9 ++ {{cookiecutter.repo_name}}/testing.ini | 39 +++++-- {{cookiecutter.repo_name}}/testing.yaml | 81 +++++++++++++ .../tests/base_conftest.py | 12 +- .../tests/sqlalchemy_conftest.py | 20 +++- .../tests/zodb_conftest.py | 12 +- 16 files changed, 501 insertions(+), 80 deletions(-) create mode 100644 {{cookiecutter.repo_name}}/development.yaml create mode 100644 {{cookiecutter.repo_name}}/production.yaml create mode 100644 {{cookiecutter.repo_name}}/testing.yaml diff --git a/CHANGES.txt b/CHANGES.txt index fe2f0c5..c0bb0f1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,8 @@ unreleased ---------- +- Add configuration file format choices, ini or yaml. + - Fix unwanted literal inclusion of a METAL attribute in rendered output within the Chameleon ``layout.pt`` template. diff --git a/README.rst b/README.rst index 0e53d1f..2b87761 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,7 @@ A Cookiecutter (project template) for creating a Pyramid starter project. Customizable options upon install include choice of: +* configuration file format (ini, YAML) * template language (Jinja2, Chameleon, or Mako) * persistent backend (none, SQLAlchemy with SQLite, or ZODB) * mapping of URLs to routes (if the selected persistent backend is "none" or "sqlalchemy" then URL dispatch, or if "zodb" then traversal) @@ -73,6 +74,7 @@ Usage $ env/bin/pytest #. Run your project. + (Change the "ini" suffix, if you chose a different configuration file format.) .. code-block:: bash diff --git a/cookiecutter.json b/cookiecutter.json index 749f198..6c73e56 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,6 +1,7 @@ { "project_name": "Pyramid Scaffold", "repo_name": "{{cookiecutter.project_name.lower().strip().replace(' ', '_').replace(':', '_').replace('-', '_').replace('!', '_')}}", + "configuration_file_type": ["ini", "yaml"], "template_language": ["jinja2", "chameleon", "mako"], "backend": ["none", "sqlalchemy", "zodb"], "_copy_without_render": [ diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 4671ea4..c862092 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -7,11 +7,34 @@ def main(): + tidy_config_files() clean_unused_template_settings() clean_unused_backend() display_actions_message() +def tidy_config_files(): + conf_basenames = ['development', 'production', 'testing'] + conf_exts = ['ini', 'yaml'] # Generated extensions + selected_conf_ext = '{{ cookiecutter.configuration_file_type }}' + + for some_file in conf_basenames: + for some_ext in conf_exts: + if some_ext != selected_conf_ext: + conf_path = os.path.join(WORKING, f'{some_file}.{some_ext}') + if (selected_conf_ext != 'ini' + and some_ext == 'ini' + and '{{ cookiecutter.backend }}' == 'sqlalchemy'): + # Only alembic uses the ini config files. Rename the + # standard config file to preface name with 'alembic_'. + os.rename( + conf_path, + os.path.join( + WORKING, f'alembic_{some_file}.{some_ext}')) + else: + os.unlink(conf_path) # remove unused config file + + def clean_unused_template_settings(): selected_lang = '{{ cookiecutter.template_language }}' templates = os.path.join( @@ -88,6 +111,9 @@ def delete_other_files(directory, current_prefix, rm_prefixes): delete_other_files(full_path, current_prefix, rm_prefixes) +{% set conf_prefix = ( + '' if cookiecutter.configuration_file_type == 'ini' + else 'alembic_' ) -%} def display_actions_message(): WIN = sys.platform.startswith('win') @@ -137,19 +163,19 @@ def display_actions_message(): {% if cookiecutter.backend == 'sqlalchemy' -%} Initialize and upgrade the database using Alembic. # Generate your first revision. - %(alembic_cmd)s -c development.ini revision --autogenerate -m "init" + %(alembic_cmd)s -c {{ conf_prefix }}development.ini revision --autogenerate -m "init" # Upgrade to that revision. - %(alembic_cmd)s -c development.ini upgrade head + %(alembic_cmd)s -c {{ conf_prefix }}development.ini upgrade head Load default data into the database using a script. - %(init_cmd)s development.ini + %(init_cmd)s {{ conf_prefix }}development.ini {% endif -%} Run your project's tests. %(pytest_cmd)s Run your project. - %(pserve_cmd)s development.ini + %(pserve_cmd)s development.{{ cookiecutter.configuration_file_type }} """ % env_setup) print(msg) diff --git a/tests/test_it.py b/tests/test_it.py index 5d8c6fe..fac2a17 100644 --- a/tests/test_it.py +++ b/tests/test_it.py @@ -7,7 +7,18 @@ WIN = sys.platform == 'win32' WORKING = os.path.abspath(os.path.join(os.path.curdir)) -base_files = [ +CONFIG_BASENAMES = [ + 'development', + 'production', + 'testing', +] + +CONFIG_EXTS = [ + 'ini', + 'yaml', +] + +BASE_FILES = [ '.gitignore', '/myapp/__init__.py', '/myapp/routes.py', @@ -26,13 +37,10 @@ '/tests/test_views.py', 'MANIFEST.in', 'README.md', - 'development.ini', - 'production.ini', 'pyproject.toml', - 'testing.ini', ] -sqlalchemy_files = [ +SQLALCHEMY_FILES = [ '.gitignore', '/myapp/__init__.py', '/myapp/alembic/env.py', @@ -60,13 +68,10 @@ '/tests/test_views.py', 'MANIFEST.in', 'README.md', - 'development.ini', - 'production.ini', 'pyproject.toml', - 'testing.ini', ] -zodb_files = [ +ZODB_FILES = [ '.gitignore', '/myapp/__init__.py', '/myapp/models/__init__.py', @@ -87,13 +92,22 @@ '/tests/test_views.py', 'MANIFEST.in', 'README.md', - 'development.ini', - 'production.ini', 'pyproject.toml', - 'testing.ini', ] +def build_conf_files(config_type, backend): + """Build a list of configuration file (relative) pathnames of the + configuration files the cookiecutter creates.""" + config_paths = [f'{config_basename}.{config_type}' + for config_basename in CONFIG_BASENAMES] + if config_type != 'ini' and backend == 'sqlalchemy': + # alembic needs .ini config files + config_paths.extend([f'alembic_{config_basename}.ini' + for config_basename in CONFIG_BASENAMES]) + return config_paths + + def build_files_list(root_dir): """Build a list containing relative paths to the generated files.""" file_list = [] @@ -104,11 +118,13 @@ def build_files_list(root_dir): return file_list +@pytest.mark.parametrize('config_type', CONFIG_EXTS) @pytest.mark.parametrize('template', ['jinja2', 'mako', 'chameleon']) -def test_base(cookies, venv, capfd, template): +def test_base(cookies, venv, capfd, template, config_type): result = cookies.bake(extra_context={ 'project_name': 'Test Project', 'template_language': template, + 'configuration_file_type': config_type, 'backend': 'none', 'repo_name': 'myapp', }) @@ -117,15 +133,18 @@ def test_base(cookies, venv, capfd, template): out, err = capfd.readouterr() + expected_files = BASE_FILES.copy() + expected_files.extend(build_conf_files(config_type, 'none')) + if WIN: assert 'Scripts\\pserve' in out - for idx, base_file in enumerate(base_files): - base_files[idx] = base_file.replace('/', '\\') - base_files.sort() - + for idx, base_file in enumerate(expected_files): + expected_files[idx] = base_file.replace('/', '\\') else: assert 'bin/pserve' in out + expected_files.sort() + # Get the file list generated by cookiecutter. Differs based on backend. files = build_files_list(str(result.project_path)) files.sort() @@ -134,11 +153,12 @@ def test_base(cookies, venv, capfd, template): if template == 'chameleon': template = 'pt' - for idx, base_file in enumerate(base_files): + for idx, base_file in enumerate(expected_files): if 'templates' in base_file: - base_files[idx] = base_files[idx].split('.')[0] + '.' + template + expected_files[idx] = expected_files[idx].split('.')[ + 0] + '.' + template - assert base_files == files + assert expected_files == files cwd = str(result.project_path) @@ -151,11 +171,13 @@ def test_base(cookies, venv, capfd, template): subprocess.check_call([venv.python, '-m', 'pytest', '-q'], cwd=cwd) +@pytest.mark.parametrize('config_type', CONFIG_EXTS) @pytest.mark.parametrize('template', ['jinja2', 'mako', 'chameleon']) -def test_zodb(cookies, venv, capfd, template): +def test_zodb(cookies, venv, capfd, template, config_type): result = cookies.bake(extra_context={ 'project_name': 'Test Project', 'template_language': template, + 'configuration_file_type': config_type, 'backend': 'zodb', 'repo_name': 'myapp', }) @@ -164,14 +186,18 @@ def test_zodb(cookies, venv, capfd, template): out, err = capfd.readouterr() + expected_files = ZODB_FILES.copy() + expected_files.extend(build_conf_files(config_type, 'zodb')) + if WIN: assert 'Scripts\\pserve' in out - for idx, zodb_file in enumerate(zodb_files): - zodb_files[idx] = zodb_file.replace('/', '\\') - zodb_files.sort() + for idx, zodb_file in enumerate(expected_files): + expected_files[idx] = zodb_file.replace('/', '\\') else: assert 'bin/pserve' in out + expected_files.sort() + # Get the file list generated by cookiecutter. Differs based on backend. files = build_files_list(str(result.project_path)) files.sort() @@ -180,11 +206,12 @@ def test_zodb(cookies, venv, capfd, template): if template == 'chameleon': template = 'pt' - for idx, zodb_file in enumerate(zodb_files): + for idx, zodb_file in enumerate(expected_files): if 'templates' in zodb_file: - zodb_files[idx] = zodb_files[idx].split('.')[0] + '.' + template + expected_files[idx] = expected_files[idx].split('.')[ + 0] + '.' + template - assert zodb_files == files + assert expected_files == files cwd = str(result.project_path) @@ -197,11 +224,13 @@ def test_zodb(cookies, venv, capfd, template): subprocess.check_call([venv.python, '-m', 'pytest', '-q'], cwd=cwd) +@pytest.mark.parametrize('config_type', CONFIG_EXTS) @pytest.mark.parametrize('template', ['jinja2', 'mako', 'chameleon']) -def test_sqlalchemy(cookies, venv, capfd, template): +def test_sqlalchemy(cookies, venv, capfd, template, config_type): result = cookies.bake(extra_context={ 'project_name': 'Test Project', 'template_language': template, + 'configuration_file_type': config_type, 'backend': 'sqlalchemy', 'repo_name': 'myapp', }) @@ -210,14 +239,18 @@ def test_sqlalchemy(cookies, venv, capfd, template): out, err = capfd.readouterr() + expected_files = SQLALCHEMY_FILES.copy() + expected_files.extend(build_conf_files(config_type, 'sqlalchemy')) + if WIN: assert 'Scripts\\pserve' in out - for idx, sqlalchemy_file in enumerate(sqlalchemy_files): - sqlalchemy_files[idx] = sqlalchemy_file.replace('/', '\\') - sqlalchemy_files.sort() + for idx, sqlalchemy_file in enumerate(expected_files): + expected_files[idx] = sqlalchemy_file.replace('/', '\\') else: assert 'bin/pserve' in out + expected_files.sort() + # Get the file list generated by cookiecutter. Differs based on backend. files = build_files_list(str(result.project_path)) files.sort() @@ -226,12 +259,12 @@ def test_sqlalchemy(cookies, venv, capfd, template): if template == 'chameleon': template = 'pt' - for idx, sqlalchemy_file in enumerate(sqlalchemy_files): + for idx, sqlalchemy_file in enumerate(expected_files): if 'templates' in sqlalchemy_file: - sqlalchemy_files[idx] = sqlalchemy_files[idx].split('.')[ + expected_files[idx] = expected_files[idx].split('.')[ 0] + '.' + template - assert sqlalchemy_files == files + assert expected_files == files cwd = str(result.project_path) @@ -241,12 +274,17 @@ def test_sqlalchemy(cookies, venv, capfd, template): venv.install(os.environ['OVERRIDE_PYRAMID'], editable=True) venv.install(cwd + '[testing]', editable=True) + + if config_type == 'ini': + alembic_cfg_file = 'testing.ini' + else: + alembic_cfg_file = 'alembic_testing.ini' create_migration_script = textwrap.dedent( - ''' + f''' import alembic.config import alembic.command - config = alembic.config.Config('testing.ini') + config = alembic.config.Config('{alembic_cfg_file}') alembic.command.revision( config, autogenerate=True, diff --git a/{{cookiecutter.repo_name}}/README.md b/{{cookiecutter.repo_name}}/README.md index 7094e0c..923a5b2 100644 --- a/{{cookiecutter.repo_name}}/README.md +++ b/{{cookiecutter.repo_name}}/README.md @@ -28,24 +28,27 @@ ``` {% if cookiecutter.backend == 'sqlalchemy' -%} +{% set conf_prefix = ( + '' if cookiecutter.configuration_file_type == 'ini' + else 'alembic_' ) -%} - Initialize and upgrade the database using Alembic. - Generate your first revision. ``` - env/bin/alembic -c development.ini revision --autogenerate -m "init" + env/bin/alembic -c {{ conf_prefix }}development.ini revision --autogenerate -m "init" ``` - Upgrade to that revision. ``` - env/bin/alembic -c development.ini upgrade head + env/bin/alembic -c {{ conf_prefix }}development.ini upgrade head ``` - Load default data into the database using a script. ``` - env/bin/initialize_{{ cookiecutter.repo_name }}_db development.ini + env/bin/initialize_{{ cookiecutter.repo_name }}_db {{ conf_prefix }}development.ini ``` {% endif -%} @@ -58,5 +61,5 @@ - Run your project. ``` - env/bin/pserve development.ini + env/bin/pserve development.{{ cookiecutter.configuration_file_type }} ``` diff --git a/{{cookiecutter.repo_name}}/development.ini b/{{cookiecutter.repo_name}}/development.ini index 778e98f..f6af95c 100644 --- a/{{cookiecutter.repo_name}}/development.ini +++ b/{{cookiecutter.repo_name}}/development.ini @@ -1,11 +1,20 @@ +{# The whitespace control strategy is to end all blocks with "-%", and + _not_ use "%-" at the beginning of blocks. This means that block + content must always end with the desired trailing whitespace, + including empty lines. +-#} +{% set alembic_only = cookiecutter.configuration_file_type != 'ini' -%} +{% if not alembic_only -%} ### # app configuration # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html ### +{% endif -%} [app:main] use = egg:{{ cookiecutter.repo_name }} +{% if not alembic_only -%} pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -14,18 +23,20 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_debugtoolbar +{% endif -%} {% if cookiecutter.backend == 'sqlalchemy' -%} - sqlalchemy.url = sqlite:///%(here)s/{{ cookiecutter.repo_name }}.sqlite -{% elif cookiecutter.backend == 'zodb' -%} +{% elif cookiecutter.backend == 'zodb' -%} zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 -{% endif %} -{%- if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} +{% endif -%} + +{% if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' -%} retry.attempts = 3 {% endif -%} +{% if not alembic_only -%} # By default, the toolbar only appears for clients from IP addresses # '127.0.0.1' and '::1'. # debugtoolbar.hosts = 127.0.0.1 ::1 @@ -34,6 +45,7 @@ retry.attempts = 3 [pshell] setup = {{ cookiecutter.repo_name }}.pshell.setup +{% endif -%} {% endif -%} {% if cookiecutter.backend == 'sqlalchemy' -%} [alembic] @@ -43,6 +55,7 @@ file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s {% endif -%} +{% if not alembic_only -%} ### # wsgi server configuration ### @@ -51,18 +64,25 @@ file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s use = egg:waitress#main listen = localhost:6543 +{% endif -%} ### # logging configuration # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html ### [loggers] -{%- if cookiecutter.backend == 'sqlalchemy' %} +{% if cookiecutter.backend == 'sqlalchemy' -%} +{% if alembic_only -%} +keys = root, sqlalchemy, alembic + +{% else -%} keys = root, {{ cookiecutter.repo_name }}, sqlalchemy, alembic -{%- else %} + +{% endif -%} +{% else -%} keys = root, {{ cookiecutter.repo_name }} -{%- endif %} +{% endif -%} [handlers] keys = console @@ -73,11 +93,13 @@ keys = generic level = INFO handlers = console +{% if not alembic_only -%} [logger_{{ cookiecutter.repo_name }}] level = DEBUG handlers = qualname = {{ cookiecutter.repo_name }} +{% endif -%} {% if cookiecutter.backend == 'sqlalchemy' -%} [logger_sqlalchemy] level = WARN diff --git a/{{cookiecutter.repo_name}}/development.yaml b/{{cookiecutter.repo_name}}/development.yaml new file mode 100644 index 0000000..77feb38 --- /dev/null +++ b/{{cookiecutter.repo_name}}/development.yaml @@ -0,0 +1,94 @@ +--- +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### +app: + use: "egg:{{ cookiecutter.repo_name }}" + + # Pyramid settings; run in development mode + pyramid.reload_templates: true + pyramid.debug_authorization: false + pyramid.debug_notfound: false + pyramid.debug_routematch: false + pyramid.default_locale_name: "en" + pyramid.includes: + - "pyramid_debugtoolbar" + +{%- if cookiecutter.backend == 'sqlalchemy' %} + + sqlalchemy.url: "sqlite:///{{ cookiecutter.repo_name }}.sqlite" +{%- elif cookiecutter.backend == 'zodb' %} + + zodbconn.uri: "file://Data.fs?connection_cache_size=20000" +{%- endif -%} + +{%- if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} + + retry.attempts: "3" +{%- endif %} +{%- macro cident() -%} + {% if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} # {% else -%} +# {% endif -%} +{% endmacro %} + +{{ cident() }}# By default, the toolbar only appears for clients from IP addresses +{{ cident() }}# '127.0.0.1' and '::1'. +{{ cident() }}#debugtoolbar.hosts: "127.0.0.1 ::1" + +{%- if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} + +pshell: + setup: "{{ cookiecutter.repo_name }}.pshell.setup" +{%- endif %} +{%- if cookiecutter.backend == 'sqlalchemy' %} + +alembic: + # path to migration scripts + script_location: "{{ cookiecutter.repo_name }}/alembic" + file_template: "%%(year)d%%(month).2d%%(day).2d_%%(rev)s" + # file_template: "%%(rev)s_%%(slug)s" + +{%- endif %} + +server: + use: "egg:waitress#main" + listen: "localhost:6543" + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +# https://docs.python.org/3/library/logging.config.html#dictionary-schema-details +### +logging: + version: 1 + disable_existing_loggers: false + formatters: + generic: + format: >- + %(asctime)s %(levelname)-5.5s + [%(name)s:%(lineno)s][%(threadName)s] %(message)s + handlers: + console: + class: logging.StreamHandler + stream: ext://sys.stderr + formatter: generic + root: + level: INFO + handlers: + - console + loggers: + logger_{{ cookiecutter.repo_name }}: + level: DEBUG + qualname: "{{ cookiecutter.repo_name }}" +{%- if cookiecutter.backend == 'sqlalchemy' %} + logger_sqlalchemy: + # level: INFO # logs SQL queries. + # level: DEBUG # logs SQL queries and results. + # level: WARN # logs neither. (Recommended for production systems.) + level: WARN + qualname: sqlalchemy.engine + logger_alembic: + level: INFO + qualname: alembic +{%- endif %} diff --git a/{{cookiecutter.repo_name}}/production.ini b/{{cookiecutter.repo_name}}/production.ini index 6a8e5ba..b49e7a1 100644 --- a/{{cookiecutter.repo_name}}/production.ini +++ b/{{cookiecutter.repo_name}}/production.ini @@ -1,28 +1,41 @@ +{# The whitespace control strategy is to end all blocks with "-%", and + _not_ use "%-" at the beginning of blocks. This means that block + content must always end with the desired trailing whitespace, + including empty lines. +-#} +{% set alembic_only = cookiecutter.configuration_file_type != 'ini' -%} +{% if not alembic_only -%} ### # app configuration # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html ### +{% endif -%} [app:main] use = egg:{{ cookiecutter.repo_name }} +{% if not alembic_only -%} pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en +{% endif -%} {% if cookiecutter.backend == 'sqlalchemy' -%} - sqlalchemy.url = sqlite:///%(here)s/{{ cookiecutter.repo_name }}.sqlite -{% elif cookiecutter.backend == 'zodb' -%} +{% elif cookiecutter.backend == 'zodb' and not alembic_only -%} zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 -{% endif %} -{%- if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} +{% endif -%} + +{% if cookiecutter.backend == 'sqlalchemy' + or (cookiecutter.backend == 'zodb' and not alembic_only) -%} retry.attempts = 3 +{% endif -%} +{% if not alembic_only -%} [pshell] setup = {{ cookiecutter.repo_name }}.pshell.setup @@ -35,6 +48,7 @@ file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s {% endif -%} +{% if not alembic_only -%} ### # wsgi server configuration ### @@ -43,18 +57,25 @@ file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s use = egg:waitress#main listen = *:6543 +{% endif -%} ### # logging configuration # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html ### [loggers] -{%- if cookiecutter.backend == 'sqlalchemy' %} +{% if cookiecutter.backend == 'sqlalchemy' -%} +{% if alembic_only -%} +keys = root, sqlalchemy, alembic + +{% else -%} keys = root, {{ cookiecutter.repo_name }}, sqlalchemy, alembic -{%- else %} + +{% endif -%} +{% else -%} keys = root, {{ cookiecutter.repo_name }} -{%- endif %} +{% endif -%} [handlers] keys = console @@ -65,11 +86,13 @@ keys = generic level = WARN handlers = console +{% if not alembic_only -%} [logger_{{ cookiecutter.repo_name }}] level = WARN handlers = qualname = {{ cookiecutter.repo_name }} +{% endif -%} {% if cookiecutter.backend == 'sqlalchemy' -%} [logger_sqlalchemy] level = WARN diff --git a/{{cookiecutter.repo_name}}/production.yaml b/{{cookiecutter.repo_name}}/production.yaml new file mode 100644 index 0000000..582a335 --- /dev/null +++ b/{{cookiecutter.repo_name}}/production.yaml @@ -0,0 +1,81 @@ +--- +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### +app: + use: "egg:{{ cookiecutter.repo_name }}" + + # Pyramid settings; run in development mode + pyramid.reload_templates: false + pyramid.debug_authorization: false + pyramid.debug_notfound: false + pyramid.debug_routematch: false + pyramid.default_locale_name: "en" + +{%- if cookiecutter.backend == 'sqlalchemy' %} + + sqlalchemy.url: "sqlite:///{{ cookiecutter.repo_name }}.sqlite" +{%- elif cookiecutter.backend == 'zodb' %} + + zodbconn.uri: "file://Data.fs?connection_cache_size=20000" +{%- endif -%} + +{%- if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} + + retry.attempts: "3" + +pshell: + setup: "{{ cookiecutter.repo_name }}.pshell.setup" +{%- endif %} +{%- if cookiecutter.backend == 'sqlalchemy' %} + +alembic: + # path to migration scripts + script_location: "{{ cookiecutter.repo_name }}/alembic" + file_template: "%%(year)d%%(month).2d%%(day).2d_%%(rev)s" + # file_template: "%%(rev)s_%%(slug)s" + +{%- endif %} + +server: + use: "egg:waitress#main" + listen: "*:6543" + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +# https://docs.python.org/3/library/logging.config.html#dictionary-schema-details +### +logging: + version: 1 + disable_existing_loggers: false + formatters: + generic: + format: >- + %(asctime)s %(levelname)-5.5s + [%(name)s:%(lineno)s][%(threadName)s] %(message)s + handlers: + console: + class: logging.StreamHandler + stream: ext://sys.stderr + formatter: generic + root: + level: WARN + handlers: + - console + loggers: + logger_{{ cookiecutter.repo_name }}: + level: WARN + qualname: "{{ cookiecutter.repo_name }}" +{%- if cookiecutter.backend == 'sqlalchemy' %} + logger_sqlalchemy: + # level: INFO # logs SQL queries. + # level: DEBUG # logs SQL queries and results. + # level: WARN # logs neither. (Recommended for production systems.) + level: WARN + qualname: sqlalchemy.engine + logger_alembic: + level: WARN + qualname: alembic +{%- endif %} diff --git a/{{cookiecutter.repo_name}}/pyproject.toml b/{{cookiecutter.repo_name}}/pyproject.toml index 5abdc48..5ac19c5 100644 --- a/{{cookiecutter.repo_name}}/pyproject.toml +++ b/{{cookiecutter.repo_name}}/pyproject.toml @@ -18,7 +18,11 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ + {%- if cookiecutter.configuration_file_type == "ini" %} "plaster_pastedeploy", + {%- else %} + "plaster_yaml", + {%- endif %} "pyramid", "pyramid_{{ cookiecutter.template_language }}", "pyramid_debugtoolbar", @@ -41,6 +45,7 @@ dependencies = [ [project.optional-dependencies] testing = [ + "plaster", "WebTest", "pytest", "pytest-cov", @@ -51,7 +56,11 @@ initialize_{{ cookiecutter.repo_name }}_db = "{{ cookiecutter.repo_name }}.scrip {% endif %} [project.entry-points."paste.app_factory"] main = "{{ cookiecutter.repo_name }}:main" +{% if cookiecutter.configuration_file_type == "yaml" %} +[project.entry-points.'plaster.loader_factory'] +'file+yaml' = 'plaster_yaml:Loader' +{% endif -%} [tool.setuptools.packages.find] exclude = ["tests"] diff --git a/{{cookiecutter.repo_name}}/testing.ini b/{{cookiecutter.repo_name}}/testing.ini index 06c9f70..3261ff6 100644 --- a/{{cookiecutter.repo_name}}/testing.ini +++ b/{{cookiecutter.repo_name}}/testing.ini @@ -1,28 +1,41 @@ +{# The whitespace control strategy is to end all blocks with "-%", and + _not_ use "%-" at the beginning of blocks. This means that block + content must always end with the desired trailing whitespace, + including empty lines. +-#} +{% set alembic_only = cookiecutter.configuration_file_type != 'ini' -%} +{% if not alembic_only -%} ### # app configuration # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html ### +{% endif -%} [app:main] use = egg:{{ cookiecutter.repo_name }} +{% if not alembic_only -%} pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en +{% endif -%} {% if cookiecutter.backend == 'sqlalchemy' -%} +sqlalchemy.url = sqlite:///testing.sqlite -sqlalchemy.url = sqlite:///%(here)s/testing.sqlite -{% elif cookiecutter.backend == 'zodb' -%} +{% elif cookiecutter.backend == 'zodb' and not alembic_only -%} +zodbconn.uri = file://Data.testing.fs?connection_cache_size=20000 -zodbconn.uri = file://%(here)s/Data.testing.fs?connection_cache_size=20000 -{% endif %} +{% endif -%} -{%- if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} +{% if cookiecutter.backend == 'sqlalchemy' + or (cookiecutter.backend == 'zodb' and not alembic_only) -%} retry.attempts = 3 +{% endif -%} +{% if not alembic_only -%} [pshell] setup = {{ cookiecutter.repo_name }}.pshell.setup @@ -35,6 +48,7 @@ file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s {% endif -%} +{% if not alembic_only -%} ### # wsgi server configuration ### @@ -43,18 +57,25 @@ file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s use = egg:waitress#main listen = localhost:6543 +{% endif -%} ### # logging configuration # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html ### [loggers] -{%- if cookiecutter.backend == 'sqlalchemy' %} +{% if cookiecutter.backend == 'sqlalchemy' -%} +{% if alembic_only -%} +keys = root, sqlalchemy, alembic + +{% else -%} keys = root, {{ cookiecutter.repo_name }}, sqlalchemy, alembic -{%- else %} + +{% endif -%} +{% else -%} keys = root, {{ cookiecutter.repo_name }} -{%- endif %} +{% endif -%} [handlers] keys = console @@ -65,11 +86,13 @@ keys = generic level = INFO handlers = console +{% if not alembic_only -%} [logger_{{ cookiecutter.repo_name }}] level = DEBUG handlers = qualname = {{ cookiecutter.repo_name }} +{% endif -%} {% if cookiecutter.backend == 'sqlalchemy' -%} [logger_sqlalchemy] level = WARN diff --git a/{{cookiecutter.repo_name}}/testing.yaml b/{{cookiecutter.repo_name}}/testing.yaml new file mode 100644 index 0000000..3f1801f --- /dev/null +++ b/{{cookiecutter.repo_name}}/testing.yaml @@ -0,0 +1,81 @@ +--- +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### +app: + use: "egg:{{ cookiecutter.repo_name }}" + + # Pyramid settings; run in development mode + pyramid.reload_templates: false + pyramid.debug_authorization: false + pyramid.debug_notfound: false + pyramid.debug_routematch: false + pyramid.default_locale_name: "en" + +{%- if cookiecutter.backend == 'sqlalchemy' %} + + sqlalchemy.url: "sqlite:///testing.sqlite" +{%- elif cookiecutter.backend == 'zodb' %} + + zodbconn.uri: "file://Data.testing.fs?connection_cache_size=20000" +{%- endif -%} + +{%- if cookiecutter.backend == 'sqlalchemy' or cookiecutter.backend == 'zodb' %} + + retry.attempts: "3" + +pshell: + setup: "{{ cookiecutter.repo_name }}.pshell.setup" +{%- endif %} +{%- if cookiecutter.backend == 'sqlalchemy' %} + +alembic: + # path to migration scripts + script_location: "{{ cookiecutter.repo_name }}/alembic" + file_template: "%%(year)d%%(month).2d%%(day).2d_%%(rev)s" + # file_template: "%%(rev)s_%%(slug)s" + +{%- endif %} + +server: + use: "egg:waitress#main" + listen: "localhost:6543" + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +# https://docs.python.org/3/library/logging.config.html#dictionary-schema-details +### +logging: + version: 1 + disable_existing_loggers: false + formatters: + generic: + format: >- + %(asctime)s %(levelname)-5.5s + [%(name)s:%(lineno)s][%(threadName)s] %(message)s + handlers: + console: + class: logging.StreamHandler + stream: ext://sys.stderr + formatter: generic + root: + level: INFO + handlers: + - console + loggers: + logger_{{ cookiecutter.repo_name }}: + level: DEBUG + qualname: "{{ cookiecutter.repo_name }}" +{%- if cookiecutter.backend == 'sqlalchemy' %} + logger_sqlalchemy: + # level: INFO # logs SQL queries. + # level: DEBUG # logs SQL queries and results. + # level: WARN # logs neither. (Recommended for production systems.) + level: WARN + qualname: sqlalchemy.engine + logger_alembic: + level: WARN + qualname: alembic +{%- endif %} diff --git a/{{cookiecutter.repo_name}}/tests/base_conftest.py b/{{cookiecutter.repo_name}}/tests/base_conftest.py index 0989fff..9d90327 100644 --- a/{{cookiecutter.repo_name}}/tests/base_conftest.py +++ b/{{cookiecutter.repo_name}}/tests/base_conftest.py @@ -9,16 +9,20 @@ def pytest_addoption(parser): + {%- if cookiecutter.configuration_file_type == 'ini' %} parser.addoption('--ini', action='store', metavar='INI_FILE') + {%- else %} + parser.addoption('--yaml', action='store', metavar='YAML_FILE') + {%- endif %} @pytest.fixture(scope='session') -def ini_file(request): +def {{ cookiecutter.configuration_file_type }}_file(request): # potentially grab this path from a pytest option - return os.path.abspath(request.config.option.ini or 'testing.ini') + return os.path.abspath(request.config.option.{{ cookiecutter.configuration_file_type }} or 'testing.{{ cookiecutter.configuration_file_type }}') @pytest.fixture(scope='session') -def app_settings(ini_file): - return get_appsettings(ini_file) +def app_settings({{ cookiecutter.configuration_file_type }}_file): + return get_appsettings({{ cookiecutter.configuration_file_type }}_file) @pytest.fixture(scope='session') def app(app_settings): diff --git a/{{cookiecutter.repo_name}}/tests/sqlalchemy_conftest.py b/{{cookiecutter.repo_name}}/tests/sqlalchemy_conftest.py index b8bf285..4b7900b 100644 --- a/{{cookiecutter.repo_name}}/tests/sqlalchemy_conftest.py +++ b/{{cookiecutter.repo_name}}/tests/sqlalchemy_conftest.py @@ -2,6 +2,7 @@ import alembic.config import alembic.command import os +import plaster from pyramid.paster import get_appsettings from pyramid.scripting import prepare from pyramid.testing import DummyRequest, testConfig @@ -15,22 +16,29 @@ def pytest_addoption(parser): + {%- if cookiecutter.configuration_file_type == 'ini' %} parser.addoption('--ini', action='store', metavar='INI_FILE') + {%- else %} + parser.addoption('--yaml', action='store', metavar='YAML_FILE') + {%- endif %} @pytest.fixture(scope='session') -def ini_file(request): +def {{ cookiecutter.configuration_file_type }}_file(request): # potentially grab this path from a pytest option - return os.path.abspath(request.config.option.ini or 'testing.ini') + return os.path.abspath(request.config.option.{{ cookiecutter.configuration_file_type }} or 'testing.{{ cookiecutter.configuration_file_type }}') @pytest.fixture(scope='session') -def app_settings(ini_file): - return get_appsettings(ini_file) +def app_settings({{ cookiecutter.configuration_file_type }}_file): + return get_appsettings({{ cookiecutter.configuration_file_type }}_file) +{% set conf_file = ( + 'testing.ini' if cookiecutter.configuration_file_type == 'ini' + else 'alembic_testing.ini' ) -%} @pytest.fixture(scope='session') -def dbengine(app_settings, ini_file): +def dbengine(app_settings): engine = models.get_engine(app_settings) - alembic_cfg = alembic.config.Config(ini_file) + alembic_cfg = alembic.config.Config('{{ conf_file }}') Base.metadata.drop_all(bind=engine) alembic.command.stamp(alembic_cfg, 'base', purge=True) diff --git a/{{cookiecutter.repo_name}}/tests/zodb_conftest.py b/{{cookiecutter.repo_name}}/tests/zodb_conftest.py index 22a330f..c61fe83 100644 --- a/{{cookiecutter.repo_name}}/tests/zodb_conftest.py +++ b/{{cookiecutter.repo_name}}/tests/zodb_conftest.py @@ -10,16 +10,20 @@ def pytest_addoption(parser): + {%- if cookiecutter.configuration_file_type == 'ini' %} parser.addoption('--ini', action='store', metavar='INI_FILE') + {%- else %} + parser.addoption('--yaml', action='store', metavar='YAML_FILE') + {%- endif %} @pytest.fixture(scope='session') -def ini_file(request): +def {{ cookiecutter.configuration_file_type }}_file(request): # potentially grab this path from a pytest option - return os.path.abspath(request.config.option.ini or 'testing.ini') + return os.path.abspath(request.config.option.{{ cookiecutter.configuration_file_type }} or 'testing.{{ cookiecutter.configuration_file_type }}') @pytest.fixture(scope='session') -def app_settings(ini_file): - return get_appsettings(ini_file) +def app_settings({{ cookiecutter.configuration_file_type }}_file): + return get_appsettings({{ cookiecutter.configuration_file_type }}_file) @pytest.fixture(scope='session') def app(app_settings):