Coverage for src/prisma/generator/utils.py: 96%
84 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-27 18:25 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-27 18:25 +0000
1from __future__ import annotations
3import os
4import re
5import shutil
6from typing import TYPE_CHECKING, Any, Dict, List, Union, TypeVar, Iterator
7from pathlib import Path
8from textwrap import dedent
10from ..utils import monkeypatch
12if TYPE_CHECKING:
13 from .models import Field, Model
16T = TypeVar('T')
18# we have to use a mapping outside of the `Sampler` class
19# to avoid https://github.com/RobertCraigie/prisma-client-py/issues/402
20SAMPLER_ITER_MAPPING: 'Dict[str, Iterator[Field]]' = {}
23class Faker:
24 """Pseudo-random re-playable data.
26 Seeds are generated using a linear congruential generator, inspired by:
27 https://stackoverflow.com/a/9024521/13923613
28 """
30 def __init__(self, seed: int = 1) -> None:
31 self._state = seed
33 def __iter__(self) -> 'Faker':
34 return self
36 def __next__(self) -> int:
37 self._state = state = (self._state * 1103515245 + 12345) & 0x7FFFFFFF
38 return state
40 def string(self) -> str:
41 return ''.join([chr(97 + int(n)) for n in str(self.integer())])
43 def boolean(self) -> bool:
44 return next(self) % 2 == 0
46 def integer(self) -> int:
47 return next(self)
49 @classmethod
50 def from_list(cls, values: List[T]) -> T:
51 # TODO: actual implementation
52 assert values, 'Expected non-empty list'
53 return values[0]
56class Sampler:
57 model: 'Model'
59 def __init__(self, model: 'Model') -> None:
60 self.model = model
61 SAMPLER_ITER_MAPPING[model.name] = model.scalar_fields
63 def get_field(self) -> 'Field':
64 mapping = SAMPLER_ITER_MAPPING
66 try:
67 field = next(mapping[self.model.name])
68 except StopIteration:
69 mapping[self.model.name] = field_iter = self.model.scalar_fields
70 field = next(field_iter)
72 return field
75def is_same_path(path: Path, other: Path) -> bool:
76 return str(path.resolve()).strip() == str(other.resolve()).strip()
79def resolve_template_path(rootdir: Path, name: Union[str, Path]) -> Path:
80 return rootdir.joinpath(remove_suffix(name, '.jinja'))
83def remove_suffix(path: Union[str, Path], suf: str) -> str:
84 """Remove a suffix from a string, if it exists."""
85 # modified from https://stackoverflow.com/a/18723694
86 if isinstance(path, Path):
87 path = str(path)
89 if suf and path.endswith(suf): 89 ↛ 91line 89 didn't jump to line 91, because the condition on line 89 was never false
90 return path[: -len(suf)]
91 return path
94def copy_tree(src: Path, dst: Path) -> None:
95 """Recursively copy the contents of a directory from src to dst.
97 This function will ignore certain compiled / cache files for convenience:
98 - *.pyc
99 - __pycache__
100 """
101 # we have to do this horrible monkeypatching as
102 # shutil makes an arbitrary call to os.makedirs
103 # which will fail if the directory already exists.
104 # the dirs_exist_ok argument does exist but was only
105 # added in python 3.8 so we cannot use that :(
107 def _patched_makedirs(
108 makedirs: Any,
109 name: str,
110 mode: int = 511,
111 exist_ok: bool = True, # noqa: ARG001
112 ) -> None:
113 makedirs(name, mode, exist_ok=True)
115 with monkeypatch(os, 'makedirs', _patched_makedirs):
116 shutil.copytree(
117 str(src),
118 str(dst),
119 ignore=shutil.ignore_patterns('*.pyc', '__pycache__'),
120 )
123def clean_multiline(string: str) -> str:
124 string = string.lstrip('\n')
125 assert string, 'Expected non-empty string'
126 lines = string.splitlines()
127 return '\n'.join([dedent(lines[0]), *lines[1:]])
130# https://github.com/nficano/humps/blob/master/humps/main.py
132ACRONYM_RE = re.compile(r'([A-Z\d]+)(?=[A-Z\d]|$)')
133PASCAL_RE = re.compile(r'([^\-_]+)')
134SPLIT_RE = re.compile(r'([\-_]*[A-Z][^A-Z]*[\-_]*)')
135UNDERSCORE_RE = re.compile(r'(?<=[^\-_])[\-_]+[^\-_]')
138def to_snake_case(input_str: str) -> str:
139 if to_camel_case(input_str) == input_str or to_pascal_case(input_str) == input_str: # if camel case or pascal case
140 input_str = ACRONYM_RE.sub(lambda m: m.group(0).title(), input_str) 140 ↛ exitline 140 didn't run the lambda on line 140
141 input_str = '_'.join(s for s in SPLIT_RE.split(input_str) if s)
142 return input_str.lower()
143 else:
144 input_str = re.sub(r'[^a-zA-Z0-9]', '_', input_str)
145 input_str = input_str.lower().strip('_')
147 return input_str
150def to_camel_case(input_str: str) -> str:
151 if len(input_str) != 0 and not input_str[:2].isupper(): 151 ↛ 153line 151 didn't jump to line 153, because the condition on line 151 was never false
152 input_str = input_str[0].lower() + input_str[1:]
153 return UNDERSCORE_RE.sub(lambda m: m.group(0)[-1].upper(), input_str)
156def to_pascal_case(input_str: str) -> str:
157 def _replace_fn(match: re.Match[str]) -> str:
158 return match.group(1)[0].upper() + match.group(1)[1:]
160 input_str = to_camel_case(PASCAL_RE.sub(_replace_fn, input_str))
161 return input_str[0].upper() + input_str[1:] if len(input_str) != 0 else input_str
164def to_constant_case(input_str: str) -> str:
165 """Converts to snake case + uppercase, examples:
167 foo_bar -> FOO_BAR
168 fooBar -> FOO_BAR
169 """
170 return to_snake_case(input_str).upper()