Skip to content

Commit

Permalink
The --json2ts-cmd option should not be validated via 'shutil.which' i…
Browse files Browse the repository at this point in the history
…n all cases (phillipdupuis#26)

* The --json2ts-cmd option should not be validated via 'shutil.which' if it contains spaces (ex: 'yarn json2ts').

In those cases we will just attempt to run it, and if the command fails we will raise a RuntimeError noting that.
  • Loading branch information
phillipdupuis committed Aug 30, 2022
1 parent c2b7473 commit 83ec9e9
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ $ pip install pydantic-to-typescript
| ‑‑module | name or filepath of the python module you would like to convert. All the pydantic models within it will be converted to typescript interfaces. Discoverable submodules will also be checked. |
| ‑‑output | name of the file the typescript definitions should be written to. Ex: './frontend/apiTypes.ts' |
| ‑‑exclude | name of a pydantic model which should be omitted from the resulting typescript definitions. This option can be defined multiple times, ex: `--exclude Foo --exclude Bar` to exclude both the Foo and Bar models from the output. |
| ‑‑json2ts‑cmd | optional, the command used to invoke json2ts. The default is 'json2ts'. Specify this if you have it installed in a strange location and need to provide the exact path (ex: /myproject/node_modules/bin/json2ts) |
| ‑‑json2ts‑cmd | optional, the command used to invoke json2ts. The default is 'json2ts'. Specify this if you have it installed locally (ex: 'yarn json2ts') or if the exact path to the executable is required (ex: /myproject/node_modules/bin/json2ts) |

---

Expand Down
34 changes: 21 additions & 13 deletions pydantic2ts/cli/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,16 @@ def extract_pydantic_models(module: ModuleType) -> List[Type[BaseModel]]:
return models


def remove_master_model_from_output(output: str) -> None:
def clean_output_file(output_filename: str) -> None:
"""
A faux 'master model' with references to all the pydantic models is necessary for generating
clean typescript definitions without any duplicates, but we don't actually want it in the
output. This function handles removing it from the generated typescript file.
Clean up the output file typescript definitions were written to by:
1. Removing the 'master model'.
This is a faux pydantic model with references to all the *actual* models necessary for generating
clean typescript definitions without any duplicates. We don't actually want it in the output, so
this function removes it from the generated typescript file.
2. Adding a banner comment with clear instructions for how to regenerate the typescript definitions.
"""
with open(output, "r") as f:
with open(output_filename, "r") as f:
lines = f.readlines()

start, end = None, None
Expand All @@ -117,7 +120,7 @@ def remove_master_model_from_output(output: str) -> None:

new_lines = banner_comment_lines + lines[:start] + lines[(end + 1) :]

with open(output, "w") as f:
with open(output_filename, "w") as f:
f.writelines(new_lines)


Expand Down Expand Up @@ -184,9 +187,10 @@ def generate_typescript_defs(
:param module: python module containing pydantic model definitions, ex: my_project.api.schemas
:param output: file that the typescript definitions will be written to
:param exclude: optional, a tuple of names for pydantic models which should be omitted from the typescript output.
:param json2ts_cmd: optional, the command that will execute json2ts. Use this if it's installed in a strange spot.
:param json2ts_cmd: optional, the command that will execute json2ts. Provide this if the executable is not
discoverable or if it's locally installed (ex: 'yarn json2ts').
"""
if not shutil.which(json2ts_cmd):
if " " not in json2ts_cmd and not shutil.which(json2ts_cmd):
raise Exception(
"json2ts must be installed. Instructions can be found here: "
"https://www.npmjs.com/package/json-schema-to-typescript"
Expand Down Expand Up @@ -214,13 +218,15 @@ def generate_typescript_defs(
f'{json2ts_cmd} -i {schema_file_path} -o {output} --bannerComment ""'
)

shutil.rmtree(schema_dir)

if json2ts_exit_code == 0:
remove_master_model_from_output(output)
clean_output_file(output)
logger.info(f"Saved typescript definitions to {output}.")
else:
logger.error(f"{json2ts_cmd} failed with exit code {json2ts_exit_code}.")

shutil.rmtree(schema_dir)
raise RuntimeError(
f'"{json2ts_cmd}" failed with exit code {json2ts_exit_code}.'
)


def parse_cli_args(args: List[str] = None) -> argparse.Namespace:
Expand Down Expand Up @@ -252,7 +258,9 @@ def parse_cli_args(args: List[str] = None) -> argparse.Namespace:
"--json2ts-cmd",
dest="json2ts_cmd",
default="json2ts",
help="path to the json-schema-to-typescript executable.\n" "(default: json2ts)",
help="path to the json-schema-to-typescript executable.\n"
"Provide this if it's not discoverable or if it's only installed locally (example: 'yarn json2ts').\n"
"(default: json2ts)",
)
return parser.parse_args(args)

Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def readme():


classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
Expand All @@ -25,7 +25,7 @@ def readme():

setup(
name="pydantic-to-typescript",
version="1.0.9",
version="1.0.10",
description="Convert pydantic models to typescript interfaces",
license="MIT",
long_description=readme(),
Expand Down
32 changes: 26 additions & 6 deletions tests/test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,36 @@ def test_calling_from_python(tmpdir):


def test_error_if_json2ts_not_installed(tmpdir):
with pytest.raises(Exception) as exc:
module_path = get_input_module("single_module")
output_path = tmpdir.join(f"cli_single_module.ts").strpath
json2ts_cmd = "someCommandWhichDefinitelyDoesNotExist"
generate_typescript_defs(module_path, output_path, json2ts_cmd=json2ts_cmd)
module_path = get_input_module("single_module")
output_path = tmpdir.join(f"cli_single_module.ts").strpath

# If the json2ts command has no spaces and the executable cannot be found,
# that means the user either hasn't installed json-schema-to-typescript or they made a typo.
# We should raise a descriptive error with installation instructions.
invalid_global_cmd = "someCommandWhichDefinitelyDoesNotExist"
with pytest.raises(Exception) as exc1:
generate_typescript_defs(
module_path,
output_path,
json2ts_cmd=invalid_global_cmd,
)
assert (
str(exc.value)
str(exc1.value)
== "json2ts must be installed. Instructions can be found here: https://www.npmjs.com/package/json-schema-to-typescript"
)

# But if the command DOES contain spaces (ex: "yarn json2ts") they're likely using a locally installed CLI.
# We should not be validating the command in this case.
# Instead we should just be *trying* to run it and checking the exit code.
invalid_local_cmd = "yaaaarn json2tsbutwithatypo"
with pytest.raises(RuntimeError) as exc2:
generate_typescript_defs(
module_path,
output_path,
json2ts_cmd=invalid_local_cmd,
)
assert str(exc2.value).startswith(f'"{invalid_local_cmd}" failed with exit code ')


def test_error_if_invalid_module_path(tmpdir):
with pytest.raises(ModuleNotFoundError):
Expand Down

0 comments on commit 83ec9e9

Please sign in to comment.