diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..8e10e96 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,99 @@ +name: "🐞 Bug report" +description: "Report a reproducible problem with the library" +type: Bug +labels: + - status:needs-triage + +body: + - type: markdown + attributes: + value: | + **Thanks for taking the time to file a bug!** + Please complete **all required sections**β€”incomplete reports will be sent back for more information. + + - type: checkboxes + id: confirmations + attributes: + label: "Before submitting" + options: + - label: "I'm using the **latest released** version of the library" + required: true + - label: "I've searched [open issues](issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) and found no duplicate" + required: true + + - type: textarea + id: description + attributes: + label: "Describe the bug, tell us what went wrong" + placeholder: "A clear and concise description of what went wrong. What did you expect to happen vs. what actually happened." + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: "Reproduction snippet" + description: "How can we reproduce the bug?" + placeholder: | + Provide the cli command that reproduces the bug: + ```bash + # paste command here + ``` + or the smallest possible script that reproduces the bug: + ```python + # paste code here + ``` + validations: + required: false + + - type: input + id: lib_version + attributes: + label: "Library version" + placeholder: "e.g. 1.4.2" + validations: + required: true + + - type: input + id: python_version + attributes: + label: "Python version" + placeholder: "e.g. 3.12.0" + validations: + required: true + + - type: input + id: os + attributes: + label: "Operating system" + placeholder: "e.g. Ubuntu 22.04 LTS / MacOS 14.3" + validations: + required: false + + - type: textarea + id: logs + attributes: + label: "Stack trace / error output" + description: "Paste any relevant logs here." + render: shell + validations: + required: false + + - type: textarea + id: extra + attributes: + label: "Additional context & screenshots" + placeholder: "Anything else that might help us debug." + validations: + required: false + + - type: dropdown + id: contribution + attributes: + label: Would you like to help fix this issue? + description: We welcome contributions and can provide guidance for first-time contributors! + options: + - "Not at this time" + - "Yes, I'd like to contribute" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..15add23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: πŸ’¬ Ask a question + url: https://github.com/Pipelex/kajson/discussions + about: "Please start a Discussion instead of filing a blank issue." diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..8d1a966 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,56 @@ +name: "✨ Feature request" +description: "Suggest an idea or improvement for the library" +type: Feature +labels: + - status:needs-triage + +body: + - type: markdown + attributes: + value: | + **Thanks for contributing an idea!** + Please fill in the sections below so we can understand and prioritise your request. + + - type: checkboxes + id: confirmations + attributes: + label: "Before submitting" + options: + - label: "I've searched [open issues](issues?q=is%3Aissue%20state%3Aopen%20type%3AFeature) and found no similar request" + required: true + - label: "I'm willing to start a discussion or contribute code" + required: false + + - type: textarea + id: problem + attributes: + label: "Problem / motivation" + placeholder: "What problem does this feature solve? Who is affected and why?" + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: "Proposed solution" + placeholder: "Describe the feature you'd like to see." + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: "Alternatives considered" + placeholder: "Any work-arounds you've tried or other approaches you considered." + validations: + required: false + + - type: dropdown + id: contribution + attributes: + label: Would you like to help implement this feature? + options: + - "Not at this time" + - "Yes, I'd like to contribute" + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/general.yml b/.github/ISSUE_TEMPLATE/general.yml new file mode 100644 index 0000000..0c90775 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general.yml @@ -0,0 +1,50 @@ +name: "πŸ“ General issue" +description: "Use this for questions, docs tweaks, refactors, or anything that isn't a Bug or Feature request." +type: Task +labels: + - status:needs-triage + +body: + - type: markdown + attributes: + value: | + **Thanks for opening an issue!** + This form is for ideas or tasks that don't fit the Bug or Feature templates. + + - type: checkboxes + id: confirmations + attributes: + label: "Before submitting" + options: + - label: "I've checked [open issues](issues?q=is%3Aissue%20state%3Aopen) and found no similar item" + required: true + - label: "It's not really a bug report or a feature request" + required: false + + - type: textarea + id: summary + attributes: + label: "What would you like to discuss or change?" + placeholder: | + A clear, concise description of the question, enhancement, refactor, or documentation update. + validations: + required: true + + - type: textarea + id: context + attributes: + label: "Relevant context (optional)" + placeholder: | + Links, screenshots, or any additional details that will help us understand. + validations: + required: false + + - type: dropdown + id: contribution + attributes: + label: Would you like to help drive or implement this? + options: + - "Not at this time" + - "Yes, I'd like to contribute" + validations: + required: false diff --git a/.github/kajson_labels.json b/.github/kajson_labels.json new file mode 100644 index 0000000..9c9df12 --- /dev/null +++ b/.github/kajson_labels.json @@ -0,0 +1,97 @@ +[ + { + "name": "priority:P0", + "color": "B60205", + "description": "Critical β€” stop the line" + }, + { + "name": "priority:P1", + "color": "D93F0B", + "description": "High priority" + }, + { + "name": "priority:P2", + "color": "E36209", + "description": "Normal priority" + }, + { + "name": "priority:P3", + "color": "FBCA04", + "description": "Low priority" + }, + { + "name": "status:needs-triage", + "color": "A2BFFC", + "description": "Awaiting triage" + }, + { + "name": "status:needs-info", + "color": "1E90FF", + "description": "Needs more information" + }, + { + "name": "status:blocked", + "color": "004385", + "description": "Work is blocked" + }, + { + "name": "status:in-progress", + "color": "0969DA", + "description": "Currently being worked on" + }, + { + "name": "status:review", + "color": "6CA4F8", + "description": "Awaiting code review" + }, + { + "name": "status:done", + "color": "14866D", + "description": "Completed / merged" + }, + { + "name": "status:duplicate", + "color": "6F42C1", + "description": "May close as soon as it's verified." + }, + { + "name": "status:not-planned", + "color": "6A737D", + "description": "Signals a likely won't-fix before formal closure." + }, + { + "name": "status:invalid", + "color": "E4E669", + "description": "Needs more info or out of scope." + }, + { + "name": "area:core", + "color": "005630", + "description": "Core library logic" + }, + { + "name": "area:examples", + "color": "005630", + "description": "Examples" + }, + { + "name": "area:docs", + "color": "28A745", + "description": "Documentation" + }, + { + "name": "area:tests", + "color": "7FDA9E", + "description": "Unit / integration / e2e tests" + }, + { + "name": "good first issue", + "color": "FFD33D", + "description": "Great for new contributors" + }, + { + "name": "help wanted", + "color": "FFEA7F", + "description": "Maintainers would love help" + } +] \ No newline at end of file diff --git a/.github/workflows/deploy-doc.yml b/.github/workflows/deploy-docs.yml similarity index 95% rename from .github/workflows/deploy-doc.yml rename to .github/workflows/deploy-docs.yml index 6e10897..34a42ff 100644 --- a/.github/workflows/deploy-doc.yml +++ b/.github/workflows/deploy-docs.yml @@ -33,4 +33,4 @@ jobs: run: uv pip install -e ".[docs]" - name: Deploy documentation - run: make doc-deploy + run: make docs-deploy diff --git a/CHANGELOG.md b/CHANGELOG.md index 56a09f3..cf23ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## Unreleased + +### πŸš€ New Features + +- **GitHub Issue Templates**: Added bug report, feature request, and general issue templates to GitHub repository for better issue management +- **API Documentation**: Added KajsonManager API reference documentation (Issue #26) + +### πŸ“ Changes + +- **Makefile Updates**: Renamed 'doc' targets to 'docs', including 'docs-check' and 'docs-deploy' for better consistency +- **UniversalJSONEncoder Cleanup**: Removed unused logger from UniversalJSONEncoder class (Issue #27) +- **Performance Fix**: In json_encoder.py, in _get_type_module(), the regex compilation should be at the module level (#28) + +### πŸ”’ Security + +- **Documentation**: Added security considerations section to README regarding deserializing untrusted JSON data + ## [v0.3.1] - 2025-07-10 - Fix documentation URL in `pyproject.toml` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46e1c36..f932eff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,9 +69,9 @@ make cov # Run tests with coverage make cm # Run tests with coverage and missing lines # Documentation -make doc # Serve documentation locally with mkdocs -make doc-check # Check documentation build -make doc-deploy # Deploy documentation to GitHub Pages +make dosc # Serve documentation locally with mkdocs +make docs-check # Check documentation build +make docs-deploy # Deploy documentation to GitHub Pages # Cleanup make cleanall # Remove all derived files and virtual env diff --git a/Makefile b/Makefile index c252d65..904c191 100644 --- a/Makefile +++ b/Makefile @@ -62,12 +62,19 @@ make cov - Run tests with coverage stats (use PKG=module.na make cov-missing - Run tests with coverage and missing lines (use PKG=module.name to scope coverage) make cm - Shorthand -> cov-missing +make check-unused-imports - Check for unused imports without fixing +make fix-unused-imports - Fix unused imports with ruff +make fui - Shorthand -> fix-unused-imports +make check-TODOs - Check for TODOs + +make docs - Serve documentation with mkdocs +make docs-check - Check documentation build with mkdocs +make docs-deploy - Deploy documentation with mkdocs + make check - Shorthand -> format lint mypy make c - Shorthand -> check make cc - Shorthand -> cleanderived check make li - Shorthand -> lock install -make check-unused-imports - Check for unused imports without fixing -make fix-unused-imports - Fix unused imports with ruff endef export HELP @@ -78,8 +85,9 @@ export HELP cleanderived cleanenv cleanall \ test test-with-prints tp cov cov-missing cm \ check c cc li \ - check-unused-imports fix-unused-imports \ - check-uv check-TODOs + check-unused-imports fix-unused-imports fui \ + check-uv check-TODOs \ + docs docs-check docs-deploy all help: @echo "$$HELP" @@ -259,42 +267,52 @@ merge-check-mypy: env $(VENV_MYPY) --config-file pyproject.toml ########################################################################################## -### SHORTHANDS +### MISCELLANEOUS ########################################################################################## check-unused-imports: env $(call PRINT_TITLE,"Checking for unused imports without fixing") - @$(VENV_RUFF) check --select=F401 --no-fix . - -c: format lint pyright mypy - @echo "> done: c = check" + $(VENV_RUFF) check --select=F401 --no-fix . -cc: cleanderived c - @echo "> done: cc = cleanderived check" - -check: cleanderived check-unused-imports c - @echo "> done: check" +fix-unused-imports: env + $(call PRINT_TITLE,"Fixing unused imports") + $(VENV_RUFF) check --select=F401 --fix . -li: lock install - @echo "> done: lock install" +fui: fix-unused-imports + @echo "> done: fui = fix-unused-imports" check-TODOs: env $(call PRINT_TITLE,"Checking for TODOs") @$(VENV_RUFF) check --select=TD -v . -fix-unused-imports: env - $(call PRINT_TITLE,"Fixing unused imports") - @$(VENV_RUFF) check --select=F401 --fix -v . +########################################################################################## +### DOCUMENTATION +########################################################################################## -doc: env +docs: env $(call PRINT_TITLE,"Serving documentation with mkdocs") $(VENV_MKDOCS) serve -doc-check: env +docs-check: env $(call PRINT_TITLE,"Checking documentation build with mkdocs") $(VENV_MKDOCS) build --strict -doc-deploy: env +docs-deploy: env $(call PRINT_TITLE,"Deploying documentation with mkdocs") $(VENV_MKDOCS) gh-deploy --force --clean - \ No newline at end of file + +########################################################################################## +### SHORTHANDS +########################################################################################## + +c: format lint pyright mypy + @echo "> done: c = check" + +cc: cleanderived c + @echo "> done: cc = cleanderived check" + +check: cleanderived check-unused-imports c + @echo "> done: check" + +li: lock install + @echo "> done: lock install" diff --git a/README.md b/README.md index c57b761..bf6606f 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,17 @@ Kajson extends the standard JSON encoder/decoder by: 4. **Pydantic Integration**: Special handling for Pydantic models and validation 5. **Class Registry**: Maintains a registry of dynamically created classes that aren't available in standard module paths, enabling serialization/deserialization in distributed systems and runtime scenarios +## ⚠️ Security Considerations + +**Warning**: Instantiating classes using `__class__` and `__module__` attributes poses a security threat when deserializing untrusted JSON data. Malicious JSON could potentially instantiate arbitrary classes and execute code. + +Only use Kajson to deserialize JSON from trusted sources. For untrusted data, consider: +- Validating JSON structure before deserialization +- Using a whitelist of allowed classes +- Sanitizing input data + +For more discussion on this topic, see [this discussion thread](https://github.com/Pipelex/kajson/discussions/44). + ## πŸ“š Use Cases - **REST APIs**: Serialize Pydantic models for API responses diff --git a/docs/pages/api/manager.md b/docs/pages/api/manager.md new file mode 100644 index 0000000..39e0c90 --- /dev/null +++ b/docs/pages/api/manager.md @@ -0,0 +1,131 @@ +# KajsonManager API Reference + +The `KajsonManager` class provides a singleton interface for managing kajson operations, including class registry management and logger configuration. + +## KajsonManager Class + +### Constructor + +```python +def __init__( + self, + logger_channel_name: Optional[str] = None, + class_registry: Optional[ClassRegistryAbstract] = None, +) -> None +``` + +Initialize the KajsonManager singleton instance. + +**Parameters:** + +- `logger_channel_name`: Name of the logger channel (default: "kajson") +- `class_registry`: Custom class registry implementation (default: ClassRegistry()) + +!!! note + KajsonManager is a singleton class. Multiple calls to the constructor will return the same instance. + +### Class Methods + +#### get_instance + +```python +@classmethod +def get_instance(cls) -> KajsonManager +``` + +Get the singleton instance of KajsonManager. Creates one if it doesn't exist. + +**Returns:** The singleton KajsonManager instance + +**Example:** + +```python +from kajson.kajson_manager import KajsonManager + +manager = KajsonManager.get_instance() +``` + +#### teardown + +```python +@classmethod +def teardown(cls) -> None +``` + +Destroy the singleton instance. Useful for testing or cleanup scenarios. + +**Example:** + +```python +from kajson.kajson_manager import KajsonManager + +# Clean up the singleton instance +KajsonManager.teardown() +``` + +#### get_class_registry + +```python +@classmethod +def get_class_registry(cls) -> ClassRegistryAbstract +``` + +Get the class registry from the singleton instance. + +**Returns:** The class registry instance used for managing custom type serialization + +**Example:** + +```python +from kajson.kajson_manager import KajsonManager + +registry = KajsonManager.get_class_registry() +``` + +## Usage Examples + +### Basic Usage + +```python +from kajson.kajson_manager import KajsonManager + +# Get the singleton instance +manager = KajsonManager.get_instance() + +# Access the class registry +registry = manager._class_registry +# or use the class method +registry = KajsonManager.get_class_registry() +``` + +### Custom Configuration + +```python +from kajson.kajson_manager import KajsonManager +from kajson.class_registry import ClassRegistry + +# Initialize with custom logger channel +manager = KajsonManager(logger_channel_name="my_logger") + +# Or with custom class registry +custom_registry = ClassRegistry() +manager = KajsonManager(class_registry=custom_registry) +``` + +### Testing and Cleanup + +```python +from kajson.kajson_manager import KajsonManager + +# In test setup - ensure clean state +KajsonManager.teardown() + +# Use the manager in tests +manager = KajsonManager.get_instance() + +# In test teardown +KajsonManager.teardown() +``` + +!!! tip + The KajsonManager is primarily used internally by kajson. Most users won't need to interact with it directly unless they're implementing custom serialization logic or need to access the class registry programmatically. \ No newline at end of file diff --git a/kajson/json_encoder.py b/kajson/json_encoder.py index 09c9ffd..27fe65b 100644 --- a/kajson/json_encoder.py +++ b/kajson/json_encoder.py @@ -23,7 +23,6 @@ from __future__ import annotations import json -import logging import re import warnings from typing import Any, Callable, ClassVar, Dict, Type, TypeVar, cast @@ -32,7 +31,6 @@ from kajson.exceptions import UnijsonEncoderError -ENCODER_LOGGER_CHANNEL_NAME = "kajson.encoder" IS_ENCODER_FALLBACK_ENABLED = False FALLBACK_MESSAGE = " Trying something else." @@ -65,13 +63,6 @@ class UniversalJSONEncoder(json.JSONEncoder): `kajson.dumps(obj)` """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.logger = logging.getLogger(ENCODER_LOGGER_CHANNEL_NAME) - - def log(self, message: str) -> None: - self.logger.debug(message) - # The registered encoding functions: _encoders: ClassVar[Dict[Type[Any], Callable[[Any], Dict[str, Any]]]] = {} @@ -222,6 +213,11 @@ def _get_object_module(obj: Any) -> str: # Remark 2: inspect.getmodule(obj) should work but it doesn't. +# Expressions used to find module names (compiled once at import time): +__class_expression = re.compile(r"^") +__type_expression = re.compile(r"^") + + def _get_type_module(the_type: Type[Any]) -> str: """ Get the name of the module containing the given type. @@ -233,9 +229,6 @@ def _get_type_module(the_type: Type[Any]) -> str: # 1) Extract the name of the module from str(type). # 2) Get the chain of submodules separated by dots. # 3) Join them together while getting rid of the last one. - # Expressions used to find module names: - __class_expression = re.compile(r"^") - __type_expression = re.compile(r"^") the_type_str = str(the_type) if search_result := __class_expression.search(the_type_str): return ".".join(search_result.group(1).split(".")[:-1]) diff --git a/kajson/kajson_manager.py b/kajson/kajson_manager.py index 6d02dbb..cdcad0b 100644 --- a/kajson/kajson_manager.py +++ b/kajson/kajson_manager.py @@ -12,7 +12,11 @@ class KajsonManager(metaclass=MetaSingleton): """A singleton class for managing kajson operations.""" - def __init__(self, logger_channel_name: Optional[str] = None, class_registry: Optional[ClassRegistryAbstract] = None) -> None: + def __init__( + self, + logger_channel_name: Optional[str] = None, + class_registry: Optional[ClassRegistryAbstract] = None, + ) -> None: self.logger_channel_name = logger_channel_name or KAJSON_LOGGER_CHANNEL_NAME self._class_registry = class_registry or ClassRegistry() diff --git a/mkdocs.yml b/mkdocs.yml index e565741..1e60885 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,6 +86,7 @@ nav: - kajson module: pages/api/kajson.md - Encoder: pages/api/encoder.md - Decoder: pages/api/decoder.md + - Manager: pages/api/manager.md - Contributing: - Guidelines: contributing.md - Code of Conduct: CODE_OF_CONDUCT.md diff --git a/tests/unit/test_json_encoder.py b/tests/unit/test_json_encoder.py index 181ca82..76bad19 100644 --- a/tests/unit/test_json_encoder.py +++ b/tests/unit/test_json_encoder.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import json -import logging import warnings from typing import Any, Dict, Generator @@ -81,19 +80,6 @@ def setup_encoder() -> Generator[UniversalJSONEncoder, None, None]: class TestUniversalJSONEncoder: """Test cases for UniversalJSONEncoder class.""" - def test_encoder_initialization(self, setup_encoder: UniversalJSONEncoder) -> None: - """Test encoder initialization sets up logger correctly.""" - encoder = setup_encoder - assert isinstance(encoder.logger, logging.Logger) - assert encoder.logger.name == "kajson.encoder" - - def test_log_method(self, setup_encoder: UniversalJSONEncoder, mocker: MockerFixture) -> None: - """Test log method calls logger debug (covers line 72).""" - encoder = setup_encoder - mock_debug = mocker.patch.object(encoder.logger, "debug") - encoder.log("test message") - mock_debug.assert_called_once_with("test message") - def test_register_valid_type_and_function(self) -> None: """Test registering a valid type and encoding function."""