blob: 97c9998ac53a9f8ce062ce0944bddd1d7a5b433d [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package python
import (
"fmt"
"regexp"
"strconv"
"strings"
"go.chromium.org/luci/common/errors"
)
// canonicalVersionRE is a regular expression that can match canonical Python
// versions.
//
// This has been modified from the PEP440 canonical regular expression to
// exclude parts outside of the (major.minor.patch...) section.
var canonicalVersionRE = regexp.MustCompile(
`^([1-9]\d*!)?` +
`((0|[1-9]\d*)(\.(0|[1-9]\d*))*)` +
`(((a|b|rc)(0|[1-9]\d*))?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*))?)` +
`(\+.*)?$`)
// Version is a Python interpreter version.
//
// It is a simplified version of the Python interpreter version scheme defined
// in PEP 440: https://www.python.org/dev/peps/pep-0440/
//
// Notably, it extracts the major, minor, and patch values out of the version.
type Version struct {
Major int
Minor int
Patch int
}
// ParseVersion parses a Python version from a version string (e.g., "1.2.3").
func ParseVersion(s string) (Version, error) {
var v Version
if s == "" {
return v, nil
}
match := canonicalVersionRE.FindStringSubmatch(s)
if match == nil {
return v, errors.Reason("non-canonical Python version string: %q", s).Err()
}
parts := strings.Split(match[2], ".")
// Values are expected to parse, and will panic otherwise. This is safe
// because the value has already been determined to be canonical above.
mustParseVersion := func(value string) int {
version, err := strconv.Atoi(value)
if err != nil {
panic(fmt.Sprintf("invalid number value %q: %s", value, err))
}
return version
}
// Regexp match guarantees that "parts" will have at least one component, and
// that all components are well-formed numbers.
if len(parts) >= 3 {
v.Patch = mustParseVersion(parts[2])
}
if len(parts) >= 2 {
v.Minor = mustParseVersion(parts[1])
}
v.Major = mustParseVersion(parts[0])
if v.IsZero() {
return v, errors.Reason("version is incomplete").Err()
}
return v, nil
}
func (v Version) String() string {
if v.IsZero() {
return ""
}
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}
// IsZero returns true if the Version is empty. This is true if the Major field,
// which must be set, is empty.
func (v *Version) IsZero() bool { return v.Major <= 0 }
// PythonBase returns the base Python interpreter name for this version.
func (v *Version) PythonBase() string {
switch {
case v.IsZero():
return "python"
case v.Minor > 0:
return fmt.Sprintf("python%d.%d", v.Major, v.Minor)
default:
return fmt.Sprintf("python%d", v.Major)
}
}
// IsSatisfiedBy returns true if "other" is a suitable match for this version. A
// suitable match:
//
// - MUST have a Major version.
// - If v is zero, other is automatically suitable.
// - If v is non-zero, other must have the same Major version as v, and a
// minor/patch version that is >= v's.
func (v *Version) IsSatisfiedBy(other Version) bool {
switch {
case other.Major <= 0:
// "other" must have a Major version.
return false
case v.IsZero():
// "v" is zero (anything), so "other" satisfies it.
return true
case v.Major != other.Major:
// "other" must match "v"'s Major version precisely.
return false
case v.Minor > other.Minor:
// "v" requires a Minor version that is greater than "other"'s.
return false
case v.Minor < other.Minor:
// "v" requires a Minor version that is less than "other"'s.
return true
case v.Patch > other.Patch:
// "v" requires a Patch version that is greater than "other"'s.
return false
default:
return true
}
}
// Less returns true if "v"'s Version semantically precedes "other".
func (v *Version) Less(other *Version) bool {
switch {
case v.Major < other.Major:
return true
case v.Major > other.Major:
return false
case v.Minor < other.Minor:
return true
case v.Minor > other.Minor:
return false
default:
return (v.Patch < other.Patch)
}
}