Repository: pydantic/pydantic

Issue:
Change `make` commands order in Makefile
# Question

Currently calling `make` in project will first run all tests and only after that will run lint checks.
Is there any reason for that?
It doesn't make sense to me as code analysis are complete much faster. Now I have to either run code analyses separately before running make, or wait while all tests will pass.

So can we change this:
```bash
.PHONY: all
all: testcov lint mypy
```
to this:
```
.PHONY: all
all:  lint mypy testcov
```
?


Comments:
It's a matter of taste, I prefer running tests first.

It doesn't make much difference since all parts are pretty fast for pydantic.

Happy to change if others also want a change.

On Wed, Nov 27, 2019, 23:44 Arseny Boykov <notifications@github.com> wrote:

> Question
>
> Currently calling make in project will first run all tests and only after
> that will run lint checks.
> Is there any reason for that?
> It doesn't make sense to me as code analysis are complete much faster. Now
> I have to either run code analyses separately before running make, or wait
> while all tests will pass.
>
> So can we change this:
>
> .PHONY: all
> all: testcov lint mypy
>
> to this:
>
> .PHONY: all
> all:  lint mypy testcov
> ?
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/samuelcolvin/pydantic/issues/1039?email_source=notifications&email_token=AA62GGMGA6M4B7PW4IH3EZ3QV4A5LA5CNFSM4JSNNTRKYY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4H4RLTLA>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AA62GGNOYN6C3K7G7MTB4RDQV4A5LANCNFSM4JSNNTRA>
> .
>

I’m fine either way. I’d say I have a weak preference for the order @MrMrRobat has proposed though.

I frequently find myself hitting a stupid linting issue after running all the tests, then get annoyed because I feel like I should probably make sure everything works after fixing the linting issue by running the tests again. But it’s a pretty minor thing either way.
Okay let's change. PR welcome

On Thu, Nov 28, 2019, 09:21 dmontagu <notifications@github.com> wrote:

> I’m fine either way. I’d say I have a weak preference for the order
> @MrMrRobat <https://github.com/MrMrRobat> has proposed though.
>
> I frequently find myself hitting a stupid linting issue after running all
> the tests, then get annoyed because I feel like I should probably make sure
> everything works after fixing the linting issue by running the tests again.
> But it’s a pretty minor thing either way.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/samuelcolvin/pydantic/issues/1039?email_source=notifications&email_token=AA62GGJLA7MV7FQHPQRRWETQV6ESHA5CNFSM4JSNNTRKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEFL6U6Q#issuecomment-559409786>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AA62GGIVV2UKVG4SXJZCJ2LQV6ESHANCNFSM4JSNNTRA>
> .
>


Patch:
diff --git a/pydantic/class_validators.py b/pydantic/class_validators.py
--- a/pydantic/class_validators.py
+++ b/pydantic/class_validators.py
@@ -1,7 +1,6 @@
 import warnings
 from collections import ChainMap
 from functools import wraps
-from inspect import Signature, signature
 from itertools import chain
 from types import FunctionType
 from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union, overload
@@ -32,6 +31,8 @@ def __init__(
 
 
 if TYPE_CHECKING:
+    from inspect import Signature
+
     from .main import BaseConfig
     from .fields import ModelField
     from .types import ModelOrDc
@@ -191,6 +192,8 @@ def extract_validators(namespace: Dict[str, Any]) -> Dict[str, List[Validator]]:
 
 
 def extract_root_validators(namespace: Dict[str, Any]) -> Tuple[List[AnyCallable], List[Tuple[bool, AnyCallable]]]:
+    from inspect import signature
+
     pre_validators: List[AnyCallable] = []
     post_validators: List[Tuple[bool, AnyCallable]] = []
     for name, value in namespace.items():
@@ -231,6 +234,8 @@ def make_generic_validator(validator: AnyCallable) -> 'ValidatorCallable':
     It's done like this so validators don't all need **kwargs in their signature, eg. any combination of
     the arguments "values", "fields" and/or "config" are permitted.
     """
+    from inspect import signature
+
     sig = signature(validator)
     args = list(sig.parameters.keys())
     first_arg = args.pop(0)
@@ -254,7 +259,7 @@ def prep_validators(v_funcs: Iterable[AnyCallable]) -> 'ValidatorsList':
 all_kwargs = {'values', 'field', 'config'}
 
 
-def _generic_validator_cls(validator: AnyCallable, sig: Signature, args: Set[str]) -> 'ValidatorCallable':
+def _generic_validator_cls(validator: AnyCallable, sig: 'Signature', args: Set[str]) -> 'ValidatorCallable':
     # assume the first argument is value
     has_kwargs = False
     if 'kwargs' in args:
@@ -288,7 +293,7 @@ def _generic_validator_cls(validator: AnyCallable, sig: Signature, args: Set[str
         return lambda cls, v, values, field, config: validator(cls, v, values=values, field=field, config=config)
 
 
-def _generic_validator_basic(validator: AnyCallable, sig: Signature, args: Set[str]) -> 'ValidatorCallable':
+def _generic_validator_basic(validator: AnyCallable, sig: 'Signature', args: Set[str]) -> 'ValidatorCallable':
     has_kwargs = False
     if 'kwargs' in args:
         has_kwargs = True
diff --git a/pydantic/color.py b/pydantic/color.py
--- a/pydantic/color.py
+++ b/pydantic/color.py
@@ -42,17 +42,18 @@ def __getitem__(self, item: Any) -> Any:
         return self._tuple[item]
 
 
-r_hex_short = re.compile(r'\s*(?:#|0x)?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?\s*')
-r_hex_long = re.compile(r'\s*(?:#|0x)?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?\s*')
+# these are not compiled here to avoid import slowdown, they'll be compiled the first time they're used, then cached
+r_hex_short = r'\s*(?:#|0x)?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?\s*'
+r_hex_long = r'\s*(?:#|0x)?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?\s*'
 _r_255 = r'(\d{1,3}(?:\.\d+)?)'
 _r_comma = r'\s*,\s*'
-r_rgb = re.compile(fr'\s*rgb\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}\)\s*')
+r_rgb = fr'\s*rgb\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}\)\s*'
 _r_alpha = r'(\d(?:\.\d+)?|\.\d+|\d{1,2}%)'
-r_rgba = re.compile(fr'\s*rgba\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_alpha}\s*\)\s*')
+r_rgba = fr'\s*rgba\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_alpha}\s*\)\s*'
 _r_h = r'(-?\d+(?:\.\d+)?|-?\.\d+)(deg|rad|turn)?'
 _r_sl = r'(\d{1,3}(?:\.\d+)?)%'
-r_hsl = re.compile(fr'\s*hsl\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}\s*\)\s*')
-r_hsla = re.compile(fr'\s*hsl\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}{_r_comma}{_r_alpha}\s*\)\s*')
+r_hsl = fr'\s*hsl\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}\s*\)\s*'
+r_hsla = fr'\s*hsl\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}{_r_comma}{_r_alpha}\s*\)\s*'
 
 # colors where the two hex characters are the same, if all colors match this the short version of hex colors can be used
 repeat_colors = {int(c * 2, 16) for c in '0123456789abcdef'}
@@ -226,7 +227,7 @@ def parse_str(value: str) -> RGBA:
     else:
         return ints_to_rgba(r, g, b, None)
 
-    m = r_hex_short.fullmatch(value_lower)
+    m = re.fullmatch(r_hex_short, value_lower)
     if m:
         *rgb, a = m.groups()
         r, g, b = [int(v * 2, 16) for v in rgb]
@@ -236,7 +237,7 @@ def parse_str(value: str) -> RGBA:
             alpha = None
         return ints_to_rgba(r, g, b, alpha)
 
-    m = r_hex_long.fullmatch(value_lower)
+    m = re.fullmatch(r_hex_long, value_lower)
     if m:
         *rgb, a = m.groups()
         r, g, b = [int(v, 16) for v in rgb]
@@ -246,20 +247,20 @@ def parse_str(value: str) -> RGBA:
             alpha = None
         return ints_to_rgba(r, g, b, alpha)
 
-    m = r_rgb.fullmatch(value_lower)
+    m = re.fullmatch(r_rgb, value_lower)
     if m:
         return ints_to_rgba(*m.groups(), None)  # type: ignore
 
-    m = r_rgba.fullmatch(value_lower)
+    m = re.fullmatch(r_rgba, value_lower)
     if m:
         return ints_to_rgba(*m.groups())  # type: ignore
 
-    m = r_hsl.fullmatch(value_lower)
+    m = re.fullmatch(r_hsl, value_lower)
     if m:
         h, h_units, s, l_ = m.groups()
         return parse_hsl(h, h_units, s, l_)
 
-    m = r_hsla.fullmatch(value_lower)
+    m = re.fullmatch(r_hsla, value_lower)
     if m:
         h, h_units, s, l_, a = m.groups()
         return parse_hsl(h, h_units, s, l_, parse_float_alpha(a))
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -1,4 +1,3 @@
-import dataclasses
 from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, Optional, Type, TypeVar, Union
 
 from .class_validators import gather_all_validators
@@ -66,6 +65,8 @@ def _process_class(
     frozen: bool,
     config: Optional[Type[Any]],
 ) -> 'DataclassType':
+    import dataclasses
+
     post_init_original = getattr(_cls, '__post_init__', None)
     if post_init_original and post_init_original.__name__ == '_pydantic_post_init':
         post_init_original = None
diff --git a/pydantic/datetime_parse.py b/pydantic/datetime_parse.py
--- a/pydantic/datetime_parse.py
+++ b/pydantic/datetime_parse.py
@@ -23,7 +23,7 @@
 date_re = re.compile(r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$')
 
 time_re = re.compile(
-    r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})' r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
+    r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
 )
 
 datetime_re = re.compile(
diff --git a/pydantic/json.py b/pydantic/json.py
--- a/pydantic/json.py
+++ b/pydantic/json.py
@@ -1,5 +1,4 @@
 import datetime
-from dataclasses import asdict, is_dataclass
 from decimal import Decimal
 from enum import Enum
 from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
@@ -42,6 +41,7 @@ def isoformat(o: Union[datetime.date, datetime.time]) -> str:
 
 
 def pydantic_encoder(obj: Any) -> Any:
+    from dataclasses import asdict, is_dataclass
     from .main import BaseModel
 
     if isinstance(obj, BaseModel):
diff --git a/pydantic/networks.py b/pydantic/networks.py
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -9,22 +9,20 @@
     _BaseAddress,
     _BaseNetwork,
 )
-from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Set, Tuple, Type, Union, cast, no_type_check
+from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Pattern, Set, Tuple, Type, Union, cast, no_type_check
 
 from . import errors
 from .utils import Representation, update_not_none
 from .validators import constr_length_validator, str_validator
 
 if TYPE_CHECKING:
+    import email_validator
     from .fields import ModelField
     from .main import BaseConfig  # noqa: F401
     from .typing import AnyCallable
 
     CallableGenerator = Generator[AnyCallable, None, None]
-
-try:
-    import email_validator
-except ImportError:
+else:
     email_validator = None
 
 NetworkType = Union[str, bytes, int, Tuple[Union[str, bytes, int], Union[str, int]]]
@@ -44,27 +42,47 @@
     'validate_email',
 ]
 
-host_part_names = ('domain', 'ipv4', 'ipv6')
-url_regex = re.compile(
-    r'(?:(?P<scheme>[a-z0-9]+?)://)?'  # scheme
-    r'(?:(?P<user>[^\s:]+)(?::(?P<password>\S*))?@)?'  # user info
-    r'(?:'
-    r'(?P<ipv4>(?:\d{1,3}\.){3}\d{1,3})|'  # ipv4
-    r'(?P<ipv6>\[[A-F0-9]*:[A-F0-9:]+\])|'  # ipv6
-    r'(?P<domain>[^\s/:?#]+)'  # domain, validation occurs later
-    r')?'
-    r'(?::(?P<port>\d+))?'  # port
-    r'(?P<path>/[^\s?]*)?'  # path
-    r'(?:\?(?P<query>[^\s#]+))?'  # query
-    r'(?:#(?P<fragment>\S+))?',  # fragment
-    re.IGNORECASE,
-)
-_ascii_chunk = r'[_0-9a-z](?:[-_0-9a-z]{0,61}[_0-9a-z])?'
+
+_url_regex_cache = None
+_ascii_domain_regex_cache = None
+_int_domain_regex_cache = None
 _domain_ending = r'(?P<tld>\.[a-z]{2,63})?\.?'
-ascii_domain_regex = re.compile(fr'(?:{_ascii_chunk}\.)*?{_ascii_chunk}{_domain_ending}', re.IGNORECASE)
 
-_int_chunk = r'[_0-9a-\U00040000](?:[-_0-9a-\U00040000]{0,61}[_0-9a-\U00040000])?'
-int_domain_regex = re.compile(fr'(?:{_int_chunk}\.)*?{_int_chunk}{_domain_ending}', re.IGNORECASE)
+
+def url_regex() -> Pattern[str]:
+    global _url_regex_cache
+    if _url_regex_cache is None:
+        _url_regex_cache = re.compile(
+            r'(?:(?P<scheme>[a-z0-9]+?)://)?'  # scheme
+            r'(?:(?P<user>[^\s:]+)(?::(?P<password>\S*))?@)?'  # user info
+            r'(?:'
+            r'(?P<ipv4>(?:\d{1,3}\.){3}\d{1,3})|'  # ipv4
+            r'(?P<ipv6>\[[A-F0-9]*:[A-F0-9:]+\])|'  # ipv6
+            r'(?P<domain>[^\s/:?#]+)'  # domain, validation occurs later
+            r')?'
+            r'(?::(?P<port>\d+))?'  # port
+            r'(?P<path>/[^\s?]*)?'  # path
+            r'(?:\?(?P<query>[^\s#]+))?'  # query
+            r'(?:#(?P<fragment>\S+))?',  # fragment
+            re.IGNORECASE,
+        )
+    return _url_regex_cache
+
+
+def ascii_domain_regex() -> Pattern[str]:
+    global _ascii_domain_regex_cache
+    if _ascii_domain_regex_cache is None:
+        ascii_chunk = r'[_0-9a-z](?:[-_0-9a-z]{0,61}[_0-9a-z])?'
+        _ascii_domain_regex_cache = re.compile(fr'(?:{ascii_chunk}\.)*?{ascii_chunk}{_domain_ending}', re.IGNORECASE)
+    return _ascii_domain_regex_cache
+
+
+def int_domain_regex() -> Pattern[str]:
+    global _int_domain_regex_cache
+    if _int_domain_regex_cache is None:
+        int_chunk = r'[_0-9a-\U00040000](?:[-_0-9a-\U00040000]{0,61}[_0-9a-\U00040000])?'
+        _int_domain_regex_cache = re.compile(fr'(?:{int_chunk}\.)*?{int_chunk}{_domain_ending}', re.IGNORECASE)
+    return _int_domain_regex_cache
 
 
 class AnyUrl(str):
@@ -156,7 +174,7 @@ def validate(cls, value: Any, field: 'ModelField', config: 'BaseConfig') -> 'Any
             value = value.strip()
         url: str = cast(str, constr_length_validator(value, field, config))
 
-        m = url_regex.match(url)
+        m = url_regex().match(url)
         # the regex should always match, if it doesn't please report with details of the URL tried
         assert m, 'URL regex failed unexpectedly'
 
@@ -202,9 +220,9 @@ def validate_host(cls, parts: Dict[str, str]) -> Tuple[str, Optional[str], str,
         if host is None:
             raise errors.UrlHostError()
         elif host_type == 'domain':
-            d = ascii_domain_regex.fullmatch(host)
+            d = ascii_domain_regex().fullmatch(host)
             if d is None:
-                d = int_domain_regex.fullmatch(host)
+                d = int_domain_regex().fullmatch(host)
                 if not d:
                     raise errors.UrlHostError()
                 host_type = 'int_domain'
@@ -263,6 +281,14 @@ def stricturl(
     return type('UrlValue', (AnyUrl,), namespace)
 
 
+def import_email_validator() -> None:
+    global email_validator
+    try:
+        import email_validator
+    except ImportError as e:
+        raise ImportError('email-validator is not installed, run `pip install pydantic[email]`') from e
+
+
 class EmailStr(str):
     @classmethod
     def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
@@ -271,8 +297,7 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
     @classmethod
     def __get_validators__(cls) -> 'CallableGenerator':
         # included here and below so the error happens straight away
-        if email_validator is None:
-            raise ImportError('email-validator is not installed, run `pip install pydantic[email]`')
+        import_email_validator()
 
         yield str_validator
         yield cls.validate
@@ -295,8 +320,7 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
 
     @classmethod
     def __get_validators__(cls) -> 'CallableGenerator':
-        if email_validator is None:
-            raise ImportError('email-validator is not installed, run `pip install pydantic[email]`')
+        import_email_validator()
 
         yield cls.validate
 
@@ -394,7 +418,7 @@ def validate_email(value: Union[str]) -> Tuple[str, str]:
     See RFC 5322 but treat it with suspicion, there seems to exist no universally acknowledged test for a valid email!
     """
     if email_validator is None:
-        raise ImportError('email-validator is not installed, run `pip install pydantic[email]`')
+        import_email_validator()
 
     m = pretty_email_regex.fullmatch(value)
     name: Optional[str] = None
diff --git a/pydantic/parse.py b/pydantic/parse.py
--- a/pydantic/parse.py
+++ b/pydantic/parse.py
@@ -62,5 +62,5 @@ def load_file(
             proto = Protocol.pickle
 
     return load_str_bytes(
-        b, proto=proto, content_type=content_type, encoding=encoding, allow_pickle=allow_pickle, json_loads=json_loads,
+        b, proto=proto, content_type=content_type, encoding=encoding, allow_pickle=allow_pickle, json_loads=json_loads
     )
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -1,4 +1,3 @@
-import inspect
 import re
 import warnings
 from datetime import date, datetime, time, timedelta
@@ -451,10 +450,12 @@ def model_process_schema(
     sub-models of the returned schema will be referenced, but their definitions will not be included in the schema. All
     the definitions are returned as the second value.
     """
+    from inspect import getdoc
+
     ref_prefix = ref_prefix or default_prefix
     known_models = known_models or set()
     s = {'title': model.__config__.title or model.__name__}
-    doc = inspect.getdoc(model)
+    doc = getdoc(model)
     if doc:
         s['description'] = doc
     known_models.add(model)
diff --git a/pydantic/utils.py b/pydantic/utils.py
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -1,9 +1,5 @@
-import inspect
-import platform
-import sys
 import warnings
-from importlib import import_module
-from pathlib import Path
+from types import GeneratorType
 from typing import (
     TYPE_CHECKING,
     AbstractSet,
@@ -37,6 +33,8 @@ def import_string(dotted_path: str) -> Any:
     Stolen approximately from django. Import a dotted module path and return the attribute/class designated by the
     last name in the path. Raise ImportError if the import fails.
     """
+    from importlib import import_module
+
     try:
         module_path, class_name = dotted_path.strip(' ').rsplit('.', 1)
     except ValueError as e:
@@ -70,7 +68,7 @@ def truncate(v: Union[str], *, max_len: int = 80) -> str:
 
 
 def sequence_like(v: AnyType) -> bool:
-    return isinstance(v, (list, tuple, set, frozenset)) or inspect.isgenerator(v)
+    return isinstance(v, (list, tuple, set, frozenset, GeneratorType))
 
 
 def validate_field_name(bases: List[Type['BaseModel']], field_name: str) -> None:
@@ -337,6 +335,11 @@ def __repr_args__(self) -> 'ReprArgs':
 
 
 def version_info() -> str:
+    import platform
+    import sys
+    from importlib import import_module
+    from pathlib import Path
+
     from .main import compiled
     from .version import VERSION
 
diff --git a/pydantic/version.py b/pydantic/version.py
--- a/pydantic/version.py
+++ b/pydantic/version.py
@@ -1,5 +1,3 @@
-from distutils.version import StrictVersion
-
 __all__ = ['VERSION']
 
-VERSION = StrictVersion('1.3')
+VERSION = '1.3'


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1743,6 +1743,6 @@ class Model(BaseModel):
                 'type': 'array',
                 'items': {'type': 'integer'},
                 'uniqueItems': True,
-            },
+            }
         },
     }
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,12 +1,13 @@
 import os
 import re
 import string
+from distutils.version import StrictVersion
 from enum import Enum
 from typing import NewType, Union
 
 import pytest
 
-from pydantic import BaseModel
+from pydantic import VERSION, BaseModel
 from pydantic.color import Color
 from pydantic.dataclasses import dataclass
 from pydantic.fields import Undefined
@@ -292,3 +293,7 @@ def test_version_info():
     s = version_info()
     assert re.match(' *pydantic version: ', s)
     assert s.count('\n') == 5
+
+
+def test_version_strict():
+    assert str(StrictVersion(VERSION)) == VERSION