Skip to content

Commit

Permalink
use pydantic v2 backwards compat mode
Browse files Browse the repository at this point in the history
This should allow using the existing pydantic v1 features while
migration to v2 is not done.

Issue: GothenburgBitFactory#998
  • Loading branch information
NexAdn committed Sep 17, 2024
1 parent 2738e59 commit 9f1efaf
Show file tree
Hide file tree
Showing 13 changed files with 59 additions and 58 deletions.
47 changes: 24 additions & 23 deletions bugwarrior/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import sys
import typing

import pydantic.error_wrappers
import pydantic.v1
import pydantic.v1.error_wrappers
import taskw
import typing_extensions

Expand All @@ -16,14 +17,14 @@
log = logging.getLogger(__name__)


class StrippedTrailingSlashUrl(pydantic.AnyUrl):
class StrippedTrailingSlashUrl(pydantic.v1.AnyUrl):

@classmethod
def validate(cls, value, field, config):
return super().validate(value.rstrip('/'), field, config)


class UrlSchemeError(pydantic.errors.UrlSchemeError):
class UrlSchemeError(pydantic.v1.UrlSchemeError):
msg_template = "URL should not include scheme ('{scheme}')"


Expand All @@ -38,11 +39,11 @@ def validate_parts(

port = parts['port']
if port is not None and int(port) > 65_535:
raise pydantic.errors.UrlPortError()
raise pydantic.v1.errors.UrlPortError()

user = parts['user']
if cls.user_required and user is None:
raise pydantic.errors.UrlUserInfoError()
raise pydantic.v1.errors.UrlUserInfoError()

return parts

Expand Down Expand Up @@ -94,13 +95,13 @@ def validate(cls, path):
return expanded_path


class PydanticConfig(pydantic.BaseConfig):
class PydanticConfig(pydantic.v1.BaseConfig):
allow_mutation = False # config is faux-immutable
extra = 'forbid' # do not allow undeclared fields
validate_all = True # validate default fields


class MainSectionConfig(pydantic.BaseModel):
class MainSectionConfig(pydantic.v1.BaseModel):

class Config(PydanticConfig):
arbitrary_types_allowed = True
Expand All @@ -114,13 +115,13 @@ class Config(PydanticConfig):
# added during validation (computed field support will land in pydantic-2)
data: typing.Optional[BugwarriorData] = None

@pydantic.root_validator
@pydantic.v1.root_validator
def compute_data(cls, values):
values['data'] = BugwarriorData(get_data_path(values['taskrc']))
return values

# optional
taskrc: TaskrcPath = pydantic.Field(
taskrc: TaskrcPath = pydantic.v1.Field(
default_factory=lambda: TaskrcPath(os.getenv('TASKRC', '~/.taskrc')))
shorten: bool = False
inline_links: bool = True
Expand All @@ -141,11 +142,11 @@ def compute_data(cls, values):
log_file: typing.Optional[LoggingPath] = None


class Hooks(pydantic.BaseModel):
class Hooks(pydantic.v1.BaseModel):
pre_import: ConfigList = ConfigList([])


class Notifications(pydantic.BaseModel):
class Notifications(pydantic.v1.BaseModel):
notifications: bool = False
# Although upstream supports it, pydantic has problems with Literal[None].
backend: typing.Optional[typing_extensions.Literal[
Expand All @@ -155,7 +156,7 @@ class Notifications(pydantic.BaseModel):
only_on_new_tasks: bool = False


class SchemaBase(pydantic.BaseSettings):
class SchemaBase(pydantic.v1.BaseSettings):
class Config(PydanticConfig):
# Allow extra top-level sections so all targets don't have to be selected.
extra = 'ignore'
Expand All @@ -167,7 +168,7 @@ class Config(PydanticConfig):
class ValidationErrorEnhancedMessages(list):
""" Methods loosely adapted from pydantic.error_wrappers. """

def __init__(self, error: pydantic.ValidationError):
def __init__(self, error: pydantic.v1.ValidationError):
super().__init__(self.flatten(error))

def __str__(self):
Expand All @@ -192,17 +193,17 @@ def display_error(self, e, error, model):

def flatten(self, err, loc=None):
for error in err.raw_errors:
if isinstance(error, pydantic.error_wrappers.ErrorWrapper):
if isinstance(error, pydantic.v1.error_wrappers.ErrorWrapper):

if loc:
error_loc = loc + error.loc_tuple()
else:
error_loc = error.loc_tuple()

if isinstance(error.exc, pydantic.ValidationError):
if isinstance(error.exc, pydantic.v1.ValidationError):
yield from self.flatten(error.exc, error_loc)
else:
e = pydantic.error_wrappers.error_dict(
e = pydantic.v1.error_wrappers.error_dict(
error.exc, PydanticConfig, error_loc)
yield self.display_error(e, error, err.model)
elif isinstance(error, list):
Expand All @@ -223,7 +224,7 @@ def raise_validation_error(msg, config_path, no_errors=1):

def get_target_validator(targets):

@pydantic.root_validator(pre=True, allow_reuse=True)
@pydantic.v1.root_validator(pre=True, allow_reuse=True)
def compute_target(cls, values):
for target in targets:
values[target]['target'] = target
Expand Down Expand Up @@ -260,7 +261,7 @@ def validate_config(config: dict, main_section: str, config_path: str) -> dict:
for target, service in servicemap.items()}

# Construct Validation Model
bugwarrior_config_model = pydantic.create_model(
bugwarrior_config_model = pydantic.v1.create_model(
'bugwarriorrc',
__base__=SchemaBase,
__validators__={'compute_target': get_target_validator(targets)},
Expand All @@ -274,14 +275,14 @@ def validate_config(config: dict, main_section: str, config_path: str) -> dict:
# Convert top-level model to dict since target names are dynamic and
# a bunch of calls to getattr(config, target) inhibits readability.
return dict(bugwarrior_config_model(**config))
except pydantic.ValidationError as e:
except pydantic.v1.ValidationError as e:
errors = ValidationErrorEnhancedMessages(e)
raise_validation_error(
str(errors), config_path, no_errors=len(errors))


# Dynamically add template fields to model.
_ServiceConfig = pydantic.create_model(
_ServiceConfig = pydantic.v1.create_model(
'_ServiceConfig',
**{f'{key}_template': (typing.Optional[str], None)
for key in taskw.task.Task.FIELDS}
Expand All @@ -303,7 +304,7 @@ class ServiceConfig(_ServiceConfig): # type: ignore # (dynamic base class)
add_tags: ConfigList = ConfigList([])
description_template: typing.Optional[str] = None

@pydantic.root_validator
@pydantic.v1.root_validator
def compute_templates(cls, values):
""" Get any defined templates for configuration values.
Expand Down Expand Up @@ -337,7 +338,7 @@ def compute_templates(cls, values):
values['templates'][key] = template
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def deprecate_filter_merge_requests(cls, values):
if hasattr(cls, '_DEPRECATE_FILTER_MERGE_REQUESTS'):
if values['filter_merge_requests'] != 'Undefined':
Expand All @@ -351,7 +352,7 @@ def deprecate_filter_merge_requests(cls, values):
values['include_merge_requests'] = True
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def deprecate_project_name(cls, values):
if hasattr(cls, '_DEPRECATE_PROJECT_NAME'):
if values['project_name'] != '':
Expand Down
4 changes: 2 additions & 2 deletions bugwarrior/services/bitbucket.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import typing

import pydantic
import pydantic.v1
import requests
import typing_extensions

Expand Down Expand Up @@ -30,7 +30,7 @@ class BitbucketConfig(config.ServiceConfig):
include_merge_requests: typing.Union[bool, typing_extensions.Literal['Undefined']] = 'Undefined'
project_owner_prefix: bool = False

@pydantic.root_validator
@pydantic.v1.root_validator
def deprecate_password_authentication(cls, values):
if values['login'] != 'Undefined' or values['password'] != 'Undefined':
log.warning(
Expand Down
10 changes: 5 additions & 5 deletions bugwarrior/services/bts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys

import pydantic
import pydantic.v1
import requests
import typing_extensions

Expand All @@ -24,7 +24,7 @@
class BTSConfig(config.ServiceConfig):
service: typing_extensions.Literal['bts']

email: pydantic.EmailStr = pydantic.EmailStr('')
email: pydantic.v1.EmailStr = pydantic.EmailStr('')
packages: config.ConfigList = config.ConfigList([])

udd: bool = False
Expand All @@ -33,20 +33,20 @@ class BTSConfig(config.ServiceConfig):
ignore_pkg: config.ConfigList = config.ConfigList([])
ignore_src: config.ConfigList = config.ConfigList([])

@pydantic.root_validator
@pydantic.v1.root_validator
def require_email_or_packages(cls, values):
if not values['email'] and not values['packages']:
raise ValueError(
'section requires one of:\n email\n packages')
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def udd_needs_email(cls, values):
if values['udd'] and not values['email']:
raise ValueError("no 'email' but UDD search was requested")
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def python_version_limited(cls, values):
log.warning(
'The Debian BTS service has a dependency that has not yet been '
Expand Down
6 changes: 3 additions & 3 deletions bugwarrior/services/bz.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import xmlrpc.client

import bugzilla
import pydantic
import pydantic.v1
import pytz
import typing_extensions

Expand All @@ -16,7 +16,7 @@
log = logging.getLogger(__name__)


class OptionalSchemeUrl(pydantic.AnyUrl):
class OptionalSchemeUrl(pydantic.v1.AnyUrl):
"""
A temporary type to use during the deprecation period of scheme-less urls.
"""
Expand Down Expand Up @@ -54,7 +54,7 @@ class BugzillaConfig(config.ServiceConfig):
'PASSES_QA',
])
include_needinfos: bool = False
query_url: typing.Optional[pydantic.AnyUrl]
query_url: typing.Optional[pydantic.v1.AnyUrl]
force_rest: bool = False
advanced: bool = False

Expand Down
10 changes: 5 additions & 5 deletions bugwarrior/services/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
import urllib.parse

import pydantic
import pydantic.v1
import requests
import typing_extensions

Expand Down Expand Up @@ -41,22 +41,22 @@ class GithubConfig(config.ServiceConfig):
project_owner_prefix: bool = False
issue_urls: config.ConfigList = config.ConfigList([])

@pydantic.root_validator
@pydantic.v1.root_validator
def deprecate_password(cls, values):
if values['password'] != 'Deprecated':
log.warning(
'Basic auth is no longer supported. Please remove '
'"password" in favor of "token".')
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def require_username_or_query(cls, values):
if not values['username'] and not values['query']:
raise ValueError(
'section requires one of:\n username\n query')
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def issue_urls_consistent_with_host(cls, values):
issue_url_paths = []
for url in values['issue_urls']:
Expand All @@ -71,7 +71,7 @@ def issue_urls_consistent_with_host(cls, values):
values['issue_urls'] = issue_url_paths
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def require_username_if_include_user_repos(cls, values):
if values['include_user_repos'] and not values['username']:
raise ValueError(
Expand Down
10 changes: 5 additions & 5 deletions bugwarrior/services/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import requests
import typing

import pydantic
import pydantic.v1
import sys
import typing_extensions

Expand Down Expand Up @@ -48,7 +48,7 @@ class GitlabConfig(config.ServiceConfig):
merge_request_query: str = ''
todo_query: str = ''

@pydantic.root_validator
@pydantic.v1.root_validator
def namespace_repo_lists(cls, values):
""" Add a default namespace to a repository name. If the name already
contains a namespace, it will be returned unchanged:
Expand All @@ -64,7 +64,7 @@ def namespace_repo_lists(cls, values):
for repo in values[repolist]]
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def default_priorities(cls, values):
for task_type in ['issue', 'todo', 'mr']:
priority_field = f'default_{task_type}_priority'
Expand All @@ -74,7 +74,7 @@ def default_priorities(cls, values):
else values['default_priority'])
return values

@pydantic.root_validator
@pydantic.v1.root_validator
def filter_gitlab_dot_com(cls, values):
"""
There must be a repository filter if the host is gitlab.com.
Expand All @@ -99,7 +99,7 @@ def filter_gitlab_dot_com(cls, values):
"there are too many on gitlab.com to fetch them all.")
return values

@pydantic.validator('owned', always=True)
@pydantic.v1.validator('owned', always=True)
def require_owned(cls, v):
"""
Migrate 'owned' field from default False to default True.
Expand Down
8 changes: 4 additions & 4 deletions bugwarrior/services/jira.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import typing
from functools import reduce

import pydantic
import pydantic.v1
import typing_extensions
from dateutil.tz.tz import tzutc
from jira.client import JIRA as BaseJIRA
Expand Down Expand Up @@ -56,7 +56,7 @@ def validate(cls, extra_fields_raw):


# NOTE: replace with stdlib dataclasses.dataclass once python-3.6 is dropped
@pydantic.dataclasses.dataclass
@pydantic.v1.dataclasses.dataclass
class JiraExtraField:
label: str
keys: typing.List[str]
Expand All @@ -78,7 +78,7 @@ def extract_value(self, fields):

class JiraConfig(config.ServiceConfig):
service: typing_extensions.Literal['jira']
base_uri: pydantic.AnyUrl
base_uri: pydantic.v1.AnyUrl
username: str

password: str = ''
Expand All @@ -94,7 +94,7 @@ class JiraConfig(config.ServiceConfig):
verify_ssl: bool = True
version: int = 5

@pydantic.root_validator
@pydantic.v1.root_validator
def require_password_xor_PAT(cls, values):
if ((values['password'] and values['PAT'])
or not (values['password'] or values['PAT'])):
Expand Down
Loading

0 comments on commit 9f1efaf

Please sign in to comment.