diff --git a/.cursor/commands/supermemory.md b/.cursor/commands/supermemory.md
new file mode 100644
index 0000000..ddc4a9f
--- /dev/null
+++ b/.cursor/commands/supermemory.md
@@ -0,0 +1,150 @@
+---
+description: Initialize Supermemory with comprehensive codebase knowledge
+---
+
+# Initializing Supermemory
+
+You are initializing persistent memory for this codebase. This is not just data collection - you're building context that will make you significantly more effective across all future sessions.
+
+## Understanding Context
+
+You are a **stateful** coding agent. Users expect to work with you over extended periods - potentially the entire lifecycle of a project. Your memory is how you get better over time and maintain continuity.
+
+## What to Remember
+
+### 1. Procedures (Rules & Workflows)
+Explicit rules that should always be followed:
+- "Never commit directly to main - always use feature branches"
+- "Always run lint before tests"
+- "Use conventional commits format"
+
+### 2. Preferences (Style & Conventions)
+Project and user coding style:
+- "Prefer functional components over class components"
+- "Use early returns instead of nested conditionals"
+- "Always add JSDoc to exported functions"
+
+### 3. Architecture & Context
+How the codebase works and why:
+- "Auth system was refactored in v2.0 - old patterns deprecated"
+- "The monorepo used to have 3 modules before consolidation"
+- "This pagination bug was fixed before - similar to PR #234"
+
+## Memory Scopes
+
+**Project-scoped** (`scope: "project"`):
+- Build/test/lint commands
+- Architecture and key directories
+- Team conventions specific to this codebase
+- Technology stack and framework choices
+- Known issues and their solutions
+
+**User-scoped** (`scope: "user"`):
+- Personal coding preferences across all projects
+- Communication style preferences
+- General workflow habits
+
+## Research Approach
+
+This is a **deep research** initialization. Take your time and be thorough (~50+ tool calls). The goal is to genuinely understand the project, not just collect surface-level facts.
+
+**What to uncover:**
+- Tech stack and dependencies (explicit and implicit)
+- Project structure and architecture
+- Build/test/deploy commands and workflows
+- Contributors & team dynamics (who works on what?)
+- Commit conventions and branching strategy
+- Code evolution (major refactors, architecture changes)
+- Pain points (areas with lots of bug fixes)
+- Implicit conventions not documented anywhere
+
+## Research Techniques
+
+### File-based
+- README.md, CONTRIBUTING.md, AGENTS.md, CLAUDE.md
+- Package manifests (package.json, Cargo.toml, pyproject.toml, go.mod, Gemfile)
+- Config files (.eslintrc, tsconfig.json, .prettierrc)
+- CI/CD configs (.github/workflows/)
+
+### Git-based
+- `git log --oneline -20` - Recent history
+- `git branch -a` - Branching strategy
+- `git log --format="%s" -50` - Commit conventions
+- `git shortlog -sn --all | head -10` - Main contributors
+
+### Explore Agent
+Fire parallel explore queries for broad understanding:
+```
+Task(explore, "What is the tech stack and key dependencies?")
+Task(explore, "What is the project structure? Key directories?")
+Task(explore, "How do you build, test, and run this project?")
+Task(explore, "What are the main architectural patterns?")
+Task(explore, "What conventions or patterns are used?")
+```
+
+## How to Do Thorough Research
+
+**Don't just collect data - analyze and cross-reference.**
+
+Bad (shallow):
+- Run commands, copy output
+- List facts without understanding
+
+Good (thorough):
+- Cross-reference findings (if inconsistent, dig deeper)
+- Resolve ambiguities (don't leave questions unanswered)
+- Read actual file content, not just names
+- Look for patterns (what do commits tell you about workflow?)
+- Think like a new team member - what would you want to know?
+
+## Saving Memories
+
+Use the `supermemory` tool for each distinct insight:
+
+```
+supermemory(mode: "add", content: "...", type: "...", scope: "project")
+```
+
+**Types:**
+- `project-config` - tech stack, commands, tooling
+- `architecture` - codebase structure, key components, data flow
+- `learned-pattern` - conventions specific to this codebase
+- `error-solution` - known issues and their fixes
+- `preference` - coding style preferences (use with user scope)
+
+**Guidelines:**
+- Save each distinct insight as a separate memory
+- Be concise but include enough context to be useful
+- Include the "why" not just the "what" when relevant
+- Update memories incrementally as you research (don't wait until the end)
+
+**Good memories:**
+- "Uses Bun runtime and package manager. Commands: bun install, bun run dev, bun test"
+- "API routes in src/routes/, handlers in src/handlers/. Hono framework."
+- "Auth uses Redis sessions, not JWT. Implementation in src/lib/auth.ts"
+- "Never use `any` type - strict TypeScript. Use `unknown` and narrow."
+- "Database migrations must be backward compatible - we do rolling deploys"
+
+## Upfront Questions
+
+Before diving in, ask:
+1. "Any specific rules I should always follow?"
+2. "Preferences for how I communicate? (terse/detailed)"
+
+## Reflection Phase
+
+Before finishing, reflect:
+1. **Completeness**: Did you cover commands, architecture, conventions, gotchas?
+2. **Quality**: Are memories concise and searchable?
+3. **Scope**: Did you correctly separate project vs user knowledge?
+
+Then ask: "I've initialized memory with X insights. Want me to continue refining, or is this good?"
+
+## Your Task
+
+1. Ask upfront questions (research depth, rules, preferences)
+2. Check existing memories: `supermemory(mode: "list", scope: "project")`
+3. Research based on chosen depth
+4. Save memories incrementally as you discover insights
+5. Reflect and verify completeness
+6. Summarize what was learned and ask if user wants refinement
\ No newline at end of file
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 6b354a7..86adf1b 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -119,3 +119,5 @@ jobs:
- name: Run ruff
run: uvx ruff check json2xml tests
+ - name: Run type check
+ run: uvx ty check json2xml
diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py
index b324215..7386f99 100644
--- a/json2xml/dicttoxml.py
+++ b/json2xml/dicttoxml.py
@@ -7,7 +7,7 @@
from decimal import Decimal
from fractions import Fraction
from random import SystemRandom
-from typing import Any, Union, cast
+from typing import Any, cast
from defusedxml.minidom import parseString
@@ -43,33 +43,24 @@ def get_unique_id(element: str) -> str:
Returns:
str: The unique ID.
"""
- ids: list[str] = [] # initialize list of unique ids
- this_id = make_id(element)
- dup = True
- while dup:
- if this_id not in ids:
- dup = False
- ids.append(this_id)
- else:
- this_id = make_id(element)
- return ids[-1]
-
-
-ELEMENT = Union[
- str,
- int,
- float,
- bool,
- complex,
- Decimal,
- Fraction,
- numbers.Number,
- Sequence[Any],
- datetime.datetime,
- datetime.date,
- None,
- dict[str, Any],
-]
+ return make_id(element)
+
+
+ELEMENT = (
+ str
+ | int
+ | float
+ | bool
+ | complex
+ | Decimal
+ | Fraction
+ | numbers.Number
+ | Sequence[Any]
+ | datetime.datetime
+ | datetime.date
+ | None
+ | dict[str, Any]
+)
def get_xml_type(val: ELEMENT) -> str:
@@ -332,7 +323,7 @@ def dict2xml_str(
cdata: bool,
item_name: str,
item_wrap: bool,
- parentIsList: bool,
+ parent_is_list: bool,
parent: str = "",
list_headers: bool = False,
) -> str:
@@ -340,7 +331,6 @@ def dict2xml_str(
parse dict2xml
"""
ids: list[str] = [] # initialize list of unique ids
- ", ".join(str(key) for key in item)
subtree = "" # Initialize subtree with default empty string
if attr_type:
@@ -358,12 +348,12 @@ def dict2xml_str(
rawitem, ids, attr_type, item_func, cdata, item_wrap, item_name, list_headers=list_headers
)
- if parentIsList and list_headers:
+ if parent_is_list and list_headers:
if len(val_attr) > 0 and not item_wrap:
attrstring = make_attrstring(val_attr)
return f"<{parent}{attrstring}>{subtree}{parent}>"
return f"<{parent}>{subtree}{parent}>"
- elif item.get("@flat", False) or (parentIsList and not item_wrap):
+ elif item.get("@flat", False) or (parent_is_list and not item_wrap):
return subtree
attrstring = make_attrstring(val_attr)
@@ -553,7 +543,7 @@ def convert_list(
cdata=cdata,
item_name=item_name,
item_wrap=item_wrap,
- parentIsList=True,
+ parent_is_list=True,
parent=parent,
list_headers=list_headers
)
@@ -640,7 +630,7 @@ def dicttoxml(
item_wrap: bool = True,
item_func: Callable[[str], str] = default_item_func,
cdata: bool = False,
- xml_namespaces: dict[str, Any] = {},
+ xml_namespaces: dict[str, Any] | None = None,
list_headers: bool = False,
xpath_format: bool = False,
) -> bytes:
@@ -794,6 +784,9 @@ def dicttoxml(
]
return "".join(output).encode("utf-8")
+ if xml_namespaces is None:
+ xml_namespaces = {}
+
output = []
namespace_str = ""
for prefix in xml_namespaces:
diff --git a/json2xml/json2xml.py b/json2xml/json2xml.py
index 4800962..cbc8479 100644
--- a/json2xml/json2xml.py
+++ b/json2xml/json2xml.py
@@ -30,9 +30,14 @@ def __init__(
self.item_wrap = item_wrap
self.xpath_format = xpath_format
- def to_xml(self) -> Any | None:
+ def to_xml(self) -> str | bytes | None:
"""
Convert to xml using dicttoxml.dicttoxml and then pretty print it.
+
+ Returns:
+ str: Pretty-printed XML string when pretty=True.
+ bytes: Raw XML bytes when pretty=False.
+ None: When data is empty or None.
"""
if self.data:
xml_data = dicttoxml.dicttoxml(
diff --git a/pyproject.toml b/pyproject.toml
index c5ae32b..d20af01 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,10 +31,6 @@ dependencies = [
"defusedxml",
"urllib3",
"xmltodict>=0.12.0",
- "pytest",
- "pytest-cov",
- "coverage",
- "setuptools",
]
[project.urls]
@@ -44,8 +40,13 @@ Homepage = "https://github.com/vinitkumar/json2xml"
include = ["json2xml"]
[project.optional-dependencies]
-test = [
+dev = [
"pytest>=8.4.1",
+ "pytest-cov",
+ "pytest-xdist",
+ "coverage",
+ "ruff",
+ "setuptools",
]
[tool.pytest.ini_options]
diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py
index 1d279f8..59b484f 100644
--- a/tests/test_dict2xml.py
+++ b/tests/test_dict2xml.py
@@ -297,7 +297,7 @@ def test_dict2xml_str_list_header(self) -> None:
cdata=False,
item_name="item",
item_wrap=False,
- parentIsList=True,
+ parent_is_list=True,
parent=parent,
list_headers=True,
)
@@ -604,7 +604,7 @@ class CustomClass:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parent_is_list=False
)
def test_convert_dict_invalid_type(self) -> None:
@@ -773,50 +773,16 @@ def test_dicttoxml_with_cdata(self) -> None:
result = dicttoxml.dicttoxml(data, cdata=True, attr_type=False, root=False)
assert b"" == result
- def test_get_unique_id_with_duplicates(self) -> None:
- """Test get_unique_id when duplicates are generated."""
- # We need to modify the original get_unique_id to simulate a pre-existing ID list
- import json2xml.dicttoxml as module
-
- # Save original function
- original_get_unique_id = module.get_unique_id
-
- # Track make_id calls
- call_count = 0
- original_make_id = module.make_id
-
- def mock_make_id(element: str, start: int = 100000, end: int = 999999) -> str:
- nonlocal call_count
- call_count += 1
- if call_count == 1:
- return "test_123456" # First call - will collide
- else:
- return "test_789012" # Second call - unique
-
- # Patch get_unique_id to use a pre-populated ids list
- def patched_get_unique_id(element: str) -> str:
- # Start with a pre-existing ID to force collision
- ids = ["test_123456"]
- this_id = module.make_id(element)
- dup = True
- while dup:
- if this_id not in ids:
- dup = False
- ids.append(this_id)
- else:
- this_id = module.make_id(element) # This exercises line 52
- return ids[-1]
-
- module.make_id = mock_make_id # type: ignore[assignment]
- module.get_unique_id = patched_get_unique_id # type: ignore[assignment]
+ def test_get_unique_id_returns_valid_format(self) -> None:
+ """Test get_unique_id returns properly formatted ID."""
+ result = dicttoxml.get_unique_id("test_element")
- try:
- result = dicttoxml.get_unique_id("test")
- assert result == "test_789012"
- assert call_count == 2
- finally:
- module.make_id = original_make_id
- module.get_unique_id = original_get_unique_id
+ # Verify format: element_NNNNNN
+ assert isinstance(result, str)
+ assert result.startswith("test_element_")
+ numeric_part = result.replace("test_element_", "")
+ assert numeric_part.isdigit()
+ assert 100000 <= int(numeric_part) <= 999999
def test_convert_with_bool_direct(self) -> None:
"""Test convert function with boolean input directly."""
@@ -893,7 +859,7 @@ def test_dict2xml_str_with_attr_type(self) -> None:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parent_is_list=False
)
assert 'type="dict"' in result
@@ -908,7 +874,7 @@ def test_dict2xml_str_with_primitive_dict(self) -> None:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parent_is_list=False
)
assert "nested" in result
@@ -1035,7 +1001,7 @@ def mock_is_primitive(val: Any) -> bool:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parent_is_list=False
)
assert "test" in result
finally:
diff --git a/tests/test_json2xml.py b/tests/test_json2xml.py
index 84c06d3..66b646a 100644
--- a/tests/test_json2xml.py
+++ b/tests/test_json2xml.py
@@ -100,7 +100,7 @@ def test_no_wrapper(self) -> None:
'{"login":"mojombo","id":1,"avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4"}'
)
xmldata = json2xml.Json2xml(data, root=False, pretty=False).to_xml()
- if xmldata:
+ if xmldata and isinstance(xmldata, bytes):
assert xmldata.startswith(b'mojombo')
pytest.raises(ExpatError, xmltodict.parse, xmldata)
@@ -218,7 +218,7 @@ def test_encoding_pretty_print(self) -> None:
'{"login":"mojombo","id":1,"avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4"}'
)
xmldata = json2xml.Json2xml(data, pretty=True).to_xml()
- if xmldata:
+ if xmldata and isinstance(xmldata, str):
assert 'encoding="UTF-8"' in xmldata
def test_encoding_without_pretty_print(self) -> None:
@@ -226,14 +226,14 @@ def test_encoding_without_pretty_print(self) -> None:
'{"login":"mojombo","id":1,"avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4"}'
)
xmldata = json2xml.Json2xml(data, pretty=False).to_xml()
- if xmldata:
+ if xmldata and isinstance(xmldata, bytes):
assert b'encoding="UTF-8"' in xmldata
def test_xpath_format_basic(self) -> None:
"""Test XPath 3.1 json-to-xml format with basic types."""
data = {"name": "John", "age": 30, "active": True}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
- if xmldata:
+ if xmldata and isinstance(xmldata, bytes):
assert b'xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
assert b'John' in xmldata
assert b'30' in xmldata
@@ -243,7 +243,7 @@ def test_xpath_format_nested_dict(self) -> None:
"""Test XPath 3.1 format with nested dictionaries."""
data = {"person": {"name": "Alice", "age": 25}}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
- if xmldata:
+ if xmldata and isinstance(xmldata, bytes):
assert b'