Skip to content

Commit a9ea79d

Browse files
committed
refac: broke into modules
roken down the dotcat.py file into smaller modules: formatting.py - For output formatting functions help_text.py - For help text parsers.py - For parsing functions output_formatters.py - For formatting output data_access.py - For data access functions cli.py - For argument parsing, main, and run functions We've updated the main dotcat.py file to import from these modules and re-export everything for backward compatibility. We've updated all the test files to use the new module structure. We've updated the main.py file to import from the cli module. We've verified that all tests pass and the command works correctly. Let's complete the task by summarizing what we've done. Now that all the tests are passing and the command is working correctly, let's summarize what we've done:
1 parent 495c094 commit a9ea79d

File tree

15 files changed

+586
-529
lines changed

15 files changed

+586
-529
lines changed

src/dotcat/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
This allows the package to be run with `python -m dotcat`.
55
"""
66

7-
from dotcat.dotcat import main
7+
from dotcat.cli import main
88

99
if __name__ == "__main__":
1010
main()

src/dotcat/cli.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""
2+
Command-line interface functions for dotcat.
3+
"""
4+
5+
import sys
6+
import os
7+
import argparse
8+
from typing import List, Tuple
9+
10+
from .__version__ import __version__
11+
from .formatting import red
12+
from .help_text import HELP, USAGE
13+
from .parsers import parse_file
14+
from .output_formatters import format_output
15+
from .data_access import from_attr_chain
16+
17+
18+
def parse_args(args: List[str]) -> Tuple[str, str, str, bool]:
19+
"""
20+
Returns the filename, dotted-path, output format, and version flag.
21+
22+
Args:
23+
args: The list of command-line arguments.
24+
25+
Returns:
26+
The filename, dotted-path, output format, and version flag.
27+
"""
28+
# Handle help commands
29+
if args is None or len(args) == 0:
30+
print(HELP) # Show help for no arguments
31+
sys.exit(0)
32+
33+
# Handle explicit help requests
34+
if "help" in args or "-h" in args or "--help" in args:
35+
print(HELP) # Show help for help requests
36+
sys.exit(0)
37+
38+
parser = argparse.ArgumentParser(add_help=False)
39+
parser.add_argument("file", type=str, nargs="?", help="The file to read from")
40+
parser.add_argument(
41+
"dotted_path",
42+
type=str,
43+
nargs="?",
44+
help="The dotted-path to look up",
45+
)
46+
parser.add_argument(
47+
"--output",
48+
type=str,
49+
default="raw",
50+
help="The output format (raw, formatted, json, yaml, toml, ini)",
51+
)
52+
parser.add_argument(
53+
"--version",
54+
action="store_true",
55+
help="Show version information",
56+
)
57+
58+
parsed_args = parser.parse_args(args)
59+
return (
60+
parsed_args.file,
61+
parsed_args.dotted_path,
62+
parsed_args.output,
63+
parsed_args.version,
64+
)
65+
66+
67+
def is_likely_dot_path(arg: str) -> bool:
68+
"""
69+
Determines if an argument is likely a dotted-path rather than a file path.
70+
71+
Args:
72+
arg: The argument to check.
73+
74+
Returns:
75+
True if the argument is likely a dot path, False otherwise.
76+
"""
77+
# If it contains dots and doesn't look like a file path
78+
if "." in arg and not os.path.exists(arg):
79+
# Check if it has multiple segments separated by dots
80+
return len(arg.split(".")) > 1
81+
return False
82+
83+
84+
def run(args: List[str] = None) -> None:
85+
"""
86+
Processes the command-line arguments and prints the value from the structured data file.
87+
88+
Args:
89+
args: The list of command-line arguments.
90+
"""
91+
# validates arguments
92+
filename, lookup_chain, output_format, version_flag = parse_args(args)
93+
94+
if version_flag:
95+
print(f"dotcat version {__version__}")
96+
return
97+
98+
# Special case: If we have only one argument and it looks like a dotted-path,
99+
# treat it as the dotted-path rather than the file
100+
if filename is not None and lookup_chain is None and len(args) == 1:
101+
if is_likely_dot_path(filename):
102+
# Swap the arguments
103+
lookup_chain = filename
104+
filename = None
105+
# Now filename is None and lookup_chain is not None
106+
107+
# Handle cases where one of the required arguments is missing
108+
if lookup_chain is None or filename is None:
109+
if filename is not None and lookup_chain is None:
110+
# Case 1: File is provided but dotted-path is missing
111+
try:
112+
if os.path.exists(filename):
113+
# File exists, but dotted-path is missing
114+
print(
115+
f"Dotted-path required. Which value do you want me to look up in {filename}?"
116+
)
117+
print(f"\n$dotcat {filename} {red('<dotted-path>')}")
118+
sys.exit(2) # Invalid usage
119+
except Exception:
120+
# If there's any error checking the file, fall back to general usage message
121+
pass
122+
elif filename is None and lookup_chain is not None:
123+
# Case 2: Dotted-path is provided but file is missing
124+
# Check if the argument looks like a dotted-path (contains dots)
125+
if "." in lookup_chain:
126+
# It looks like a dotted-path, so assume the file is missing
127+
print(
128+
f"File path required. Which file contains the value at {lookup_chain}?"
129+
)
130+
print(f"\n$dotcat {red('<file>')} {lookup_chain}")
131+
sys.exit(2) # Invalid usage
132+
# Otherwise, it might be a file without an extension or something else,
133+
# so fall back to the general usage message
134+
135+
# General usage message for other cases
136+
print(USAGE) # Display usage for invalid arguments
137+
sys.exit(2) # Invalid usage
138+
139+
# gets the parsed data
140+
try:
141+
data = parse_file(filename)
142+
except FileNotFoundError as e:
143+
print(str(e))
144+
sys.exit(3) # File not found
145+
except ValueError as e:
146+
if "File is empty" in str(e):
147+
print(f"{red('[ERROR]')} {filename}: File is empty")
148+
elif "Unable to parse file" in str(e):
149+
print(f"Unable to parse file: {red(filename)}")
150+
else:
151+
print(f"{str(e)}: {red(filename)}")
152+
sys.exit(4) # Parsing error
153+
154+
# get the value at the specified key
155+
try:
156+
value = from_attr_chain(data, lookup_chain)
157+
print(format_output(value, output_format))
158+
except KeyError as e:
159+
key = e.args[0].split("'")[1] if "'" in e.args[0] else e.args[0]
160+
print(f"Key {red(key)} not found in {filename}")
161+
sys.exit(5) # Key not found
162+
163+
164+
def main() -> None:
165+
"""
166+
The main entry point of the script.
167+
"""
168+
run(sys.argv[1:])

src/dotcat/data_access.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
Data access functions for dotcat.
3+
"""
4+
5+
from typing import Any, Dict
6+
7+
LIST_ACCESS_SYMBOL = "@"
8+
SLICE_SYMBOL = ":"
9+
10+
11+
def access_list(data: Any, key: str, index: str) -> Any:
12+
"""
13+
Accesses a list within a dictionary using a key and an index or slice.
14+
15+
Args:
16+
data: The dictionary containing the list.
17+
key: The key for the list.
18+
index: The index or slice to access.
19+
20+
Returns:
21+
The accessed list item or slice.
22+
23+
Raises:
24+
KeyError: If the index is invalid or the data is not a list.
25+
"""
26+
try:
27+
if SLICE_SYMBOL in index:
28+
start, end = map(lambda x: int(x) if x else None, index.split(SLICE_SYMBOL))
29+
return data.get(key)[start:end]
30+
else:
31+
return data.get(key)[int(index)]
32+
except (IndexError, TypeError) as e:
33+
raise KeyError(f"Invalid index '{index}' for key '{key}': {str(e)}")
34+
35+
36+
def from_attr_chain(data: Dict[str, Any], lookup_chain: str) -> Any:
37+
"""
38+
Accesses a nested dictionary value with an attribute chain encoded by a dot-separated string.
39+
40+
Args:
41+
data: The dictionary to access.
42+
lookup_chain: The dotted-path string representing the nested keys.
43+
44+
Returns:
45+
The value at the specified nested key, or None if the key doesn't exist.
46+
"""
47+
keys = lookup_chain.split(".")
48+
found_keys = []
49+
50+
if data is None:
51+
chain = keys[0]
52+
raise KeyError(f"key '{chain}' not found")
53+
54+
for key in keys:
55+
if LIST_ACCESS_SYMBOL in key:
56+
key, index = key.split(LIST_ACCESS_SYMBOL)
57+
data = access_list(data, key, index)
58+
else:
59+
data = data.get(key)
60+
if data is None:
61+
full_path = ".".join(found_keys + [key])
62+
raise KeyError(f"key '{key}' not found in path '{full_path}'")
63+
found_keys.append(key)
64+
return data

0 commit comments

Comments
 (0)