Whole file

joke2k/django-environ

The author described this change as Fix `environ.Path.__eq__()` to compare paths correctly. It counts as a record because the check below fails on the code as it stood at ea308232d and passes on 27a8a8735, with nothing else changed between the two runs.

Fix saved2022-06-14
Sharing licenceMIT · LICENSE.txt
Change size+643 637

What the code was meant to do, written into the code itself as a save note

Fix `environ.Path.__eq__()` to compare paths correctly

The change

361361 def get_value(self, var, cast=None, default=NOTSET, parse_default=False):
362362 """Return value for given environment variable.
363363
364- :param var: Name of variable.
365- :param cast: Type to cast return value as.
366- :param default: If var not present in environ, return this instead.
367- :param parse_default: force to parse default..
368-
369- :returns: Value from environment or default (if set)
370- """
371-
372- logger.debug("get '{}' casted as '{}' with default '{}'".format(
373- var, cast, default
374- ))
375-
376- var_name = "{}{}".format(self.prefix, var)
377- if var_name in self.scheme:
378- var_info = self.scheme[var_name]
379-
380- try:
381- has_default = len(var_info) == 2
382- except TypeError:
383- has_default = False
384-
385- if has_default:
386- if not cast:
387- cast = var_info[0]
388-
389- if default is self.NOTSET:
390- try:
391- default = var_info[1]
392- except IndexError:
393- pass
394- else:
395- if not cast:
396- cast = var_info
397-
398- try:
399- value = self.ENVIRON[var_name]
400- except KeyError as exc:
401- if default is self.NOTSET:
402- error_msg = "Set the {} environment variable".format(var)
403- raise ImproperlyConfigured(error_msg) from exc
404-
405- value = default
406-
407- # Resolve any proxied values
408- prefix = b'$' if isinstance(value, bytes) else '$'
409- escape = rb'\$' if isinstance(value, bytes) else r'\$'
410- if hasattr(value, 'startswith') and value.startswith(prefix):
411- value = value.lstrip(prefix)
412- value = self.get_value(value, cast=cast, default=default)
413-
414- if self.escape_proxy and hasattr(value, 'replace'):
415- value = value.replace(escape, prefix)
416-
417- # Smart casting
418- if self.smart_cast:
419- if cast is None and default is not None and \
420- not isinstance(default, NoValue):
421- cast = type(default)
422-
423- value = None if default is None and value == '' else value
424-
425- if value != default or (parse_default and value):
426- value = self.parse_value(value, cast)
427-
428- return value
429-
430- # Class and static methods
431-
432- @classmethod
433- def parse_value(cls, value, cast):
434- """Parse and cast provided value
435-
436- :param value: Stringed value.
437- :param cast: Type to cast return value as.
438-
439- :returns: Casted value
440- """
441- if cast is None:
442- return value
443- elif cast is bool:
444- try:
445- value = int(value) != 0
446- except ValueError:
447- value = value.lower() in cls.BOOLEAN_TRUE_STRINGS
448- elif isinstance(cast, list):
449- value = list(map(cast[0], [x for x in value.split(',') if x]))
450- elif isinstance(cast, tuple):
451- val = value.strip('(').strip(')').split(',')
452- value = tuple(map(cast[0], [x for x in val if x]))
453- elif isinstance(cast, dict):
454- key_cast = cast.get('key', str)
455- value_cast = cast.get('value', str)
456- value_cast_by_key = cast.get('cast', dict())
457- value = dict(map(
458- lambda kv: (
459- key_cast(kv[0]),
460- cls.parse_value(
461- kv[1],
462- value_cast_by_key.get(kv[0], value_cast)
463- )
464- ),
465- [val.split('=') for val in value.split(';') if val]
466- ))
467- elif cast is dict:
468- value = dict([val.split('=') for val in value.split(',') if val])
469- elif cast is list:
470- value = [x for x in value.split(',') if x]
471- elif cast is tuple:
472- val = value.strip('(').strip(')').split(',')
473- value = tuple([x for x in val if x])
474- elif cast is float:
475- # clean string
476- float_str = re.sub(r'[^\d,.-]', '', value)
477- # split for avoid thousand separator and different
478- # locale comma/dot symbol
479- parts = re.split(r'[,.]', float_str)
480- if len(parts) == 1:
481- float_str = parts[0]
482- else:
483- float_str = "{}.{}".format(''.join(parts[0:-1]), parts[-1])
484- value = float(float_str)
485- else:
486- value = cast(value)
487- return value
488-
489- @classmethod
490- def db_url_config(cls, url, engine=None):
491- """Parse an arbitrary database URL.
492-
493- Supports the following URL schemas:
494-
495- * PostgreSQL: ``postgres[ql]?://`` or ``p[g]?sql://``
496- * PostGIS: ``postgis://``
497- * MySQL: ``mysql://`` or ``mysql2://``
498- * MySQL (GIS): ``mysqlgis://``
499- * MySQL Connector Python from Oracle: ``mysql-connector://``
500- * SQLite: ``sqlite://``
501- * SQLite with SpatiaLite for GeoDjango: ``spatialite://``
502- * Oracle: ``oracle://``
503- * Microsoft SQL Server: ``mssql://``
504- * PyODBC: ``pyodbc://``
505- * Amazon Redshift: ``redshift://``
506- * LDAP: ``ldap://``
507-
508- :param urllib.parse.ParseResult or str url:
509- Database URL to parse.
510- :param str or None engine:
511- If None, the database engine is evaluates from the ``url``.
512- :return: Parsed database URL.
513- :rtype: dict
514- """
515- if not isinstance(url, cls.URL_CLASS):
516- if url == 'sqlite://:memory:':
517- # this is a special case, because if we pass this URL into
518- # urlparse, urlparse will choke trying to interpret "memory"
519- # as a port number
520- return {
521- 'ENGINE': cls.DB_SCHEMES['sqlite'],
522- 'NAME': ':memory:'
523- }
524- # note: no other settings are required for sqlite
525- url = urlparse(url)
526-
527- config = {}
528-
529- # Remove query strings.
530- path = url.path[1:]
531- path = unquote_plus(path.split('?', 2)[0])
532-
533- if url.scheme == 'sqlite':
534- if path == '':
535- # if we are using sqlite and we have no path, then assume we
536- # want an in-memory database (this is the behaviour of
537- # sqlalchemy)
538- path = ':memory:'
539- if url.netloc:
540- warnings.warn('SQLite URL contains host component %r, '
541- 'it will be ignored' % url.netloc, stacklevel=3)
542- if url.scheme == 'ldap':
543- path = '{scheme}://{hostname}'.format(
544- scheme=url.scheme,
545- hostname=url.hostname,
546- )
547- if url.port:
548- path += ':{port}'.format(port=url.port)
549-
550- user_host = url.netloc.rsplit('@', 1)
551- if url.scheme in cls.POSTGRES_FAMILY and ',' in user_host[-1]:
552- # Parsing postgres cluster dsn
553- hinfo = list(
554- itertools.zip_longest(
555- *(
556- host.rsplit(':', 1)
557- for host in user_host[-1].split(',')
558- )
559- )
560- )
561- hostname = ','.join(hinfo[0])
562- port = ','.join(filter(None, hinfo[1])) if len(hinfo) == 2 else ''
563- else:
564- hostname = url.hostname
565- port = url.port
566-
567- # Update with environment configuration.
568- config.update({
569- 'NAME': path or '',
570- 'USER': _cast_urlstr(url.username) or '',
571- 'PASSWORD': _cast_urlstr(url.password) or '',
572- 'HOST': hostname or '',
573- 'PORT': _cast_int(port) or '',
574- })
575-
576- if (
577- url.scheme in cls.POSTGRES_FAMILY and path.startswith('/')
578- or cls.CLOUDSQL in path and path.startswith('/')
579- ):
580- config['HOST'], config['NAME'] = path.rsplit('/', 1)
581-
582- if url.scheme == 'oracle' and path == '':
583- config['NAME'] = config['HOST']
584- config['HOST'] = ''
585-
586- if url.scheme == 'oracle':
587- # Django oracle/base.py strips port and fails on non-string value
588- if not config['PORT']:
589- del (config['PORT'])
590- else:
591- config['PORT'] = str(config['PORT'])
592-
593- if url.query:
594- config_options = {}
595- for k, v in parse_qs(url.query).items():
596- if k.upper() in cls._DB_BASE_OPTIONS:
597- config.update({k.upper(): _cast(v[0])})
598- else:
599- config_options.update({k: _cast_int(v[0])})
600- config['OPTIONS'] = config_options
601-
602- if engine:
603- config['ENGINE'] = engine
604- else:
605- config['ENGINE'] = url.scheme
606-
607- if config['ENGINE'] in Env.DB_SCHEMES:
608- config['ENGINE'] = Env.DB_SCHEMES[config['ENGINE']]
609-
610- if not config.get('ENGINE', False):
611- warnings.warn("Engine not recognized from url: {}".format(config))
612- return {}
613-
614- return config
615-
616- @classmethod
617- def cache_url_config(cls, url, backend=None):
618- """Parse an arbitrary cache URL.
619-
620- :param urllib.parse.ParseResult or str url:
621- Cache URL to parse.
622- :param str or None backend:
623- If None, the backend is evaluates from the ``url``.
624- :return: Parsed cache URL.
625- :rtype: dict
626- """
627- if not isinstance(url, cls.URL_CLASS):
628- if not url:
629- return {}
630- else:
631- url = urlparse(url)
632-
633- if url.scheme not in cls.CACHE_SCHEMES:
634- raise ImproperlyConfigured(
635- 'Invalid cache schema {}'.format(url.scheme)
636- )
637-
638- location = url.netloc.split(',')
639- if len(location) == 1:
640- location = location[0]
641-
642- config = {
643- 'BACKEND': cls.CACHE_SCHEMES[url.scheme],
644- 'LOCATION': location,
645- }
646-
647- # Add the drive to LOCATION
648- if url.scheme == 'filecache':
649- config.update({
650- 'LOCATION': url.netloc + url.path,
651- })
652-
653- # urlparse('pymemcache://127.0.0.1:11211')
654- # => netloc='127.0.0.1:11211', path=''
655- #
656- # urlparse('pymemcache://memcached:11211/?key_prefix=ci')
657- # => netloc='memcached:11211', path='/'
658- #
659- # urlparse('memcache:///tmp/memcached.sock')
660- # => netloc='', path='/tmp/memcached.sock'
661- if not url.netloc and url.scheme in ['memcache', 'pymemcache']:
662- config.update({
663- 'LOCATION': 'unix:' + url.path,
664- })
665- elif url.scheme.startswith('redis'):
666- if url.hostname:
667- scheme = url.scheme.replace('cache', '')
668- else:
669- scheme = 'unix'
670- locations = [scheme + '://' + loc + url.path
671- for loc in url.netloc.split(',')]
672- if len(locations) == 1:
673- config['LOCATION'] = locations[0]
674- else:
675- config['LOCATION'] = locations
676-
677- if url.query:
678- config_options = {}
679- for k, v in parse_qs(url.query).items():
680- opt = {k.upper(): _cast(v[0])}
681- if k.upper() in cls._CACHE_BASE_OPTIONS:
682- config.update(opt)
683- else:
684- config_options.update(opt)
685- config['OPTIONS'] = config_options
686-
687- if backend:
688- config['BACKEND'] = backend
689-
690- return config
691-
692- @classmethod
693- def email_url_config(cls, url, backend=None):
694- """Parse an arbitrary email URL.
695-
696- :param urllib.parse.ParseResult or str url:
697- Email URL to parse.
698- :param str or None backend:
699- If None, the backend is evaluates from the ``url``.
700- :return: Parsed email URL.
701- :rtype: dict
702- """
703-
704- config = {}
705-
706- url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url
707-
708- # Remove query strings
709- path = url.path[1:]
710- path = unquote_plus(path.split('?', 2)[0])
711-
712- # Update with environment configuration
713- config.update({
714- 'EMAIL_FILE_PATH': path,
715- 'EMAIL_HOST_USER': _cast_urlstr(url.username),
716- 'EMAIL_HOST_PASSWORD': _cast_urlstr(url.password),
717- 'EMAIL_HOST': url.hostname,
718- 'EMAIL_PORT': _cast_int(url.port),
719- })
720-
721- if backend:
722- config['EMAIL_BACKEND'] = backend
723- elif url.scheme not in cls.EMAIL_SCHEMES:
724- raise ImproperlyConfigured('Invalid email schema %s' % url.scheme)
725- elif url.scheme in cls.EMAIL_SCHEMES:
726- config['EMAIL_BACKEND'] = cls.EMAIL_SCHEMES[url.scheme]
727-
728- if url.scheme in ('smtps', 'smtp+tls'):
729- config['EMAIL_USE_TLS'] = True
730- elif url.scheme == 'smtp+ssl':
731- config['EMAIL_USE_SSL'] = True
732-
733- if url.query:
734- config_options = {}
735- for k, v in parse_qs(url.query).items():
736- opt = {k.upper(): _cast_int(v[0])}
737- if k.upper() in cls._EMAIL_BASE_OPTIONS:
738- config.update(opt)
739- else:
740- config_options.update(opt)
741- config['OPTIONS'] = config_options
742-
743- return config
744-
745- @classmethod
746- def search_url_config(cls, url, engine=None):
747- """Parse an arbitrary search URL.
748-
749- :param urllib.parse.ParseResult or str url:
750- Search URL to parse.
751- :param str or None engine:
752- If None, the engine is evaluates from the ``url``.
753- :return: Parsed search URL.
754- :rtype: dict
755- """
756-
757- config = {}
758-
759- url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url
760-
886 further changed lines not shown

The check that tells the two apart

failpass·tests/test_path.py::test_comparison

Check file tests/test_path.py, taken without changes from the fix and copied onto the older code, so the exact same check runs against both versions.

Origin and history

The code before itea308232d474ad1eb7fc6826869baad6b7136b96
Broken version dated2022-06-14
Moduleenviron.environ
Units changedEnv, Path
Fingerprint2690f44ddd4d41b7
Checked2026-08-18 by goldset/0.1

Every field above is generated by our program. None of it is written by hand.

Other bugs found in joke2k/django-environ