From ae66980acc89f0ad508249e6f65e1bbfc22b37e8 Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 11:55:22 +0700 Subject: [PATCH 1/8] add test dependencies --- setup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 71917b3..72dcb37 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,9 @@ __version__ = '1.0.7' tests_require = [ - "flake8==3.9.2", - "pytest==7.2.2" + "flake8", + "pytest", + "pytest-cov", ] with open('README.md', 'r') as fh: @@ -22,7 +23,7 @@ install_requires=[ "PyYAML>=5.4", "requests>=2.25", - "watchgod>=0.7", + "watchfiles>=0.18", "libsass>=0.21.0" ], entry_points={ @@ -30,6 +31,7 @@ 'ntk = ntk.__main__:main', ], }, + extras_require={"test": tests_require}, packages=find_packages(), - python_requires='>=3.8' + python_requires='>=3.10' ) From b9a0b6fd827e81ad08c2bbfe5483d1a7e92ce46d Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 11:57:24 +0700 Subject: [PATCH 2/8] update python versions for tox --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 6c58a18..add8244 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,14 @@ [tox] -envlist = py37, py38, py39, py310 +envlist = py310, py311, py312, py313, py314 skip_missing_interpreters = true [gh-actions] python = - 3.7: py37 - 3.8: py38 - 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + 3.14: py314 [testenv] allowlist_externals = /usr/bin/test From bd26e6e40a5ecf4d087a949165ae17146a1a46b0 Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 11:58:24 +0700 Subject: [PATCH 3/8] improve async command and use watchfiles --- ntk/command.py | 6 ++---- tests/test_command.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ntk/command.py b/ntk/command.py index a8025e5..7c49c5a 100644 --- a/ntk/command.py +++ b/ntk/command.py @@ -5,8 +5,7 @@ import time import sass -from watchgod import awatch -from watchgod.watcher import Change +from watchfiles import awatch, Change from ntk.conf import ( Config, MEDIA_FILE_EXTENSIONS, GLOB_PATTERN, SASS_DESTINATION, SASS_SOURCE @@ -198,8 +197,7 @@ async def main(): async for changes in awatch('.'): self._handle_files_change(changes) - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) @parser_config() def compile_sass(self, parser): diff --git a/tests/test_command.py b/tests/test_command.py index 4f63f4b..afc5914 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import call, MagicMock, mock_open, patch -from watchgod.watcher import Change +from watchfiles import Change from ntk import conf from ntk.command import Command @@ -397,6 +397,15 @@ def test_watch_command_with_sass_directory_should_call_compile_sass( self.command._handle_files_change(changes) mock_compile_sass.assert_called_once() + @patch("ntk.command.asyncio.run") + @patch("ntk.command.awatch", autospec=True) + def test_watch_command_uses_asyncio_run(self, mock_awatch, mock_asyncio_run): + mock_asyncio_run.side_effect = lambda coro: coro.close() + self.command.config.parser_config(self.parser) + with patch("os.getcwd", return_value="/fake/path"): + self.command.watch(self.parser) + mock_asyncio_run.assert_called_once() + ##### # sass ##### From 7437511fc079c342860519f7be0ed3d388fb04d8 Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 12:10:49 +0700 Subject: [PATCH 4/8] improve tests --- ntk/command.py | 5 ++-- tests/test_command.py | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/ntk/command.py b/ntk/command.py index 7c49c5a..be8203a 100644 --- a/ntk/command.py +++ b/ntk/command.py @@ -20,6 +20,7 @@ level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S' ) +logging.getLogger('watchfiles').setLevel(logging.WARNING) class Command: @@ -45,10 +46,10 @@ def _handle_files_change(self, changes): for event_type, pathfile in changes: template_name = get_template_name(pathfile) if event_type in [Change.added, Change.modified]: - logging.info(f'[{self.config.env}] {str(event_type)} {template_name}') + logging.info(f'[{self.config.env}] {event_type.name.title()} {template_name}') self._push_templates([template_name], compile_sass=True) elif event_type == Change.deleted: - logging.info(f'[{self.config.env}] {str(event_type)} {template_name}') + logging.info(f'[{self.config.env}] {event_type.name.title()} {template_name}') self._delete_templates([template_name]) def _push_templates(self, template_names, compile_sass=False): diff --git a/tests/test_command.py b/tests/test_command.py index afc5914..5f59d36 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -310,6 +310,62 @@ def test_pull_command_with_configs_and_filenames_should_be_download_only_file_in mock_write_config.assert_not_called() + ##### + # push + ##### + def test_push_command_without_config_file_should_be_required_api_key_store_and_theme_id(self): + with self.assertRaises(TypeError) as error: + self.parser.apikey = None + self.parser.store = None + self.parser.theme_id = None + self.command.push(self.parser) + self.assertEqual( + str(error.exception), '[development] argument -a/--apikey, -s/--store, -t/--theme_id are required.') + + @patch("ntk.command.Command._get_accept_files", autospec=True) + def test_push_command_with_configs_and_without_filenames_should_upload_all_files( + self, mock_get_accept_files + ): + mock_get_accept_files.return_value = [ + f'{os.getcwd()}/layout/base.html', + ] + self.mock_gateway.return_value.create_or_update_template.return_value.ok = True + self.mock_gateway.return_value.create_or_update_template.return_value.headers = { + 'content-type': 'application/json; charset=utf-8'} + self.command.config.parser_config(self.parser) + self.parser.filenames = None + with patch("builtins.open", self.mock_file): + self.command.push(self.parser) + expected_call = call().create_or_update_template( + theme_id=1234, + template_name='layout/base.html', + content='{% load i18n %}\n\n
My home page
', + files={} + ) + self.assertIn(expected_call, self.mock_gateway.mock_calls) + + @patch("ntk.command.Command._get_accept_files", autospec=True) + def test_push_command_with_filenames_should_upload_only_specified_files( + self, mock_get_accept_files + ): + mock_get_accept_files.return_value = [ + f'{os.getcwd()}/layout/base.html', + ] + self.mock_gateway.return_value.create_or_update_template.return_value.ok = True + self.mock_gateway.return_value.create_or_update_template.return_value.headers = { + 'content-type': 'application/json; charset=utf-8'} + self.command.config.parser_config(self.parser) + self.parser.filenames = ['layout/base.html'] + with patch("builtins.open", self.mock_file): + self.command.push(self.parser) + expected_call = call().create_or_update_template( + theme_id=1234, + template_name='layout/base.html', + content='{% load i18n %}\n\n
My home page
', + files={} + ) + self.assertIn(expected_call, self.mock_gateway.mock_calls) + ##### # watch (_handle_files_change) ##### From 60d7e9495d196bc763383f641d0716896ef27ef8 Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 12:11:35 +0700 Subject: [PATCH 5/8] rename to Next Commerce --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f077b9..3a0bd70 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![Build Status][GHAction-image]][GHAction-link] [![CodeCov][codecov-image]][codecov-link] -# 29 Next Theme Kit +# Next Commerce Theme Kit -Theme Kit is a cross-platform command line tool to build and maintain storefront themes with [Sass Processing](#sass-processing) support on the 29 Next platform. +Theme Kit is a cross-platform command line tool to build and maintain storefront themes with [Sass Processing](#sass-processing) support on the Next Commerce platform. ## Installation From 975bb7ce0efe6d9f856495822ce84634b0964be5 Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 12:11:42 +0700 Subject: [PATCH 6/8] add claude --- CLAUDE.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6313a0b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# Next Commerce Theme Kit + +CLI tool (`ntk`) for building and maintaining storefront themes on the Next Commerce platform. Supports Sass processing via `libsass`. + +## Project Structure + +``` +ntk/ + __main__.py # Entry point + command.py # All CLI commands (watch, push, pull, checkout, init, list, sass) + conf.py # Config loading and constants + decorator.py # @parser_config decorator for command validation + gateway.py # API client for Next Commerce store + utils.py # Helpers (get_template_name, progress_bar) +tests/ + test_command.py + test_gateway.py + test_config.py + test_installer.py +``` + +## Development Setup + +### Prerequisites + +- Python 3.10 or higher — check with `python --version` +- `pip` — usually included with Python +- On macOS, Python can be installed via [Homebrew](https://brew.sh): `brew install python` +- On Windows, use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install) (recommended) or the [Windows App Store](https://apps.microsoft.com/store/detail/python-310/9PJPW5LDXLZ5) + +### First-time Setup + +```bash +# Clone the repo +git clone https://github.com/29next/theme-kit.git +cd theme-kit + +# Create and activate a virtual environment +python -m venv venv +source venv/bin/activate # macOS/Linux +# venv\Scripts\activate # Windows + +# Install the package with test dependencies +pip install -e ".[test]" +``` + +### Installing the CLI Globally (for end users) + +```bash +pip install next-theme-kit + +# Or with pipx (recommended — keeps it isolated) +pipx install next-theme-kit +``` + +## Running Tests + +```bash +pytest tests/ -v +pytest --cov=ntk --cov-report xml +``` + +## Key Dependencies + +- `watchfiles` — file watching for `ntk watch` (replaced deprecated `watchgod`) +- `libsass` — Sass/SCSS processing +- `PyYAML` — config.yml parsing +- `requests` — HTTP client for store API + +## Python Support + +Requires Python >= 3.10. Tested against 3.10, 3.11, 3.12, 3.13, 3.14 via tox and GitHub Actions. + +## Important Conventions + +- Use `asyncio.run()` for async entry points — not `get_event_loop()` (broke in Python 3.12+) +- `watchfiles.Change` is an `IntEnum` — use `event_type.name.title()` for human-readable log output, not `str(event_type)` +- `watchfiles` internal logging is suppressed via `logging.getLogger('watchfiles').setLevel(logging.WARNING)` +- Test dependencies are declared as `extras_require={"test": ...}` in `setup.py` From e52f6598e89f58bd083704d49d659a957d1e594d Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 12:13:54 +0700 Subject: [PATCH 7/8] fix release build --- .github/workflows/release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e94099..cf97772 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,10 @@ jobs: python-version: 3.12 - name: Install build dependencies - run: python -m pip install build wheel + run: python -m pip install build - name: Build distributions - shell: bash -l {0} - run: python setup.py sdist bdist_wheel + run: python -m build - name: Publish package to PyPI if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') From bd36ecd0c3219af72684f3be3c0146ab88c2c1ab Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Thu, 5 Mar 2026 13:31:59 +0700 Subject: [PATCH 8/8] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72dcb37..97aaf02 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -__version__ = '1.0.7' +__version__ = '1.1.0' tests_require = [ "flake8",