Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more meaninful error when parsing conanfile reference (closes #3204) #3410

Merged
merged 12 commits into from
Sep 20, 2018
63 changes: 38 additions & 25 deletions conans/model/ref.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections import namedtuple
import re
from six import string_types
from conans.errors import ConanException, InvalidNameException
from conans.model.version import Version

Expand All @@ -8,37 +9,44 @@ class ConanName(object):
_max_chars = 50
_min_chars = 2
_validation_pattern = re.compile("^[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{%s,%s}$"
% (_min_chars - 1, _max_chars))
% (_min_chars - 1, _max_chars - 1))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is breaking! Should we change _max_chars = 51 too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some test checking the length limit of the names (or add a new one checking the upper limit)?

Copy link
Contributor Author

@jgsogo jgsogo Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are tests to check upper and lower limits here and here, maybe in order to be backwards compatible we can make _max_chars = 51 (or greater) and be sure that it is checked and used in strings with this value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say yes, we should raise it to 51


@staticmethod
def invalid_name_message(name):
if len(name) > ConanName._max_chars:
message = ("'%s' is too long. Valid names must contain at most %s characters."
% (name, ConanName._max_chars))
elif len(name) < ConanName._min_chars:
message = ("'%s' is too short. Valid names must contain at least %s characters."
% (name, ConanName._min_chars))
def invalid_name_message(value, message=None):
message = message or "'{value}' {reason}"
if len(value) > ConanName._max_chars:
reason = "is too long. Valid names must contain at most %s characters."\
% ConanName._max_chars
elif len(value) < ConanName._min_chars:
reason = "is too short. Valid names must contain at least %s characters."\
% ConanName._min_chars
else:
message = ("'%s' is an invalid name. Valid names MUST begin with a "
"letter or number, have between %s-%s chars, including "
"letters, numbers, underscore, dot and dash"
% (name, ConanName._min_chars, ConanName._max_chars))
raise InvalidNameException(message)
reason = ("is an invalid name. Valid names MUST begin with a "
"letter, number or underscore, have between %s-%s chars, including "
"letters, numbers, underscore, dot and dash"
% (ConanName._min_chars, ConanName._max_chars))
raise InvalidNameException(message.format(value=value, reason=reason,
type=type(value).__name__))

@staticmethod
def validate_user(username):
if ConanName._validation_pattern.match(username) is None:
ConanName.invalid_name_message(username)
def validate_string(value, message=None):
"""Check for string"""
if not isinstance(value, string_types):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this checking for string, too defensive. All the fields of ConanFileReference are strings, by definition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue started because we want to provide a more meaninful message for a version = 1.0 (number) inside a conanfile.py... and we didn't want to make an implicit conversion to string. So I think we do need to check for string type (or handle the exception if the regex fails).

message = message or "'{value}' (type {type}) {reason}"
raise InvalidNameException(message.format(value=value, reason="is not a string",
type=type(value).__name__))

@staticmethod
def validate_name(name, version=False):
def validate_name(name, version=False, message=None):
"""Check for name compliance with pattern rules"""
ConanName.validate_string(name, message=message)
if name == "*":
return
if ConanName._validation_pattern.match(name) is None:
if version and name.startswith("[") and name.endswith("]"):
# TODO: Check value between brackets
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave it as TODO or check version ranges according to semver as documented (https://docs.conan.io/en/latest/mastering/version_ranges.html)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be done in a different PR, not urgent, leave it.

return
ConanName.invalid_name_message(name)
ConanName.invalid_name_message(name, message=message)


class ConanFileReference(namedtuple("ConanFileReference", "name version user channel")):
Expand All @@ -52,16 +60,20 @@ class ConanFileReference(namedtuple("ConanFileReference", "name version user cha
def __new__(cls, name, version, user, channel, revision=None):
"""Simple name creation.
@param name: string containing the desired name
@param validate: checks for valid complex name. default True
@param version: string containing the desired version
@param user: string containing the user name
@param channel: string containing the user channel
@param revision: string containing the revision (optional)
"""
ConanName.validate_name(name)
ConanName.validate_name(version, True)
ConanName.validate_name(user)
ConanName.validate_name(channel)
message = "Value provided for {item}, '{{value}}' (type {{type}}), {{reason}}"
ConanName.validate_name(name, message=message.format(item="package name"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not format every argument (this operation might be done lots of times), for every new ConanFileReference you are formating 4 strings. I know it seems little, but we need to try to keep performance and running time low.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then I'll go for a different approach

ConanName.validate_name(version, True, message=message.format(item="package version"))
ConanName.validate_name(user, message=message.format(item="package user name"))
ConanName.validate_name(channel, message=message.format(item="package user"))
version = Version(version)
obj = super(cls, ConanFileReference).__new__(cls, name, version, user, channel)
if revision:
ConanName.validate_name(revision)
ConanName.validate_name(revision, message=message.format(item="package revision"))
obj.revision = revision
return obj

Expand Down Expand Up @@ -123,7 +135,8 @@ def __new__(cls, conan, package_id):
revision = None
if "#" in package_id:
package_id, revision = package_id.rsplit("#", 1)
ConanName.validate_name(revision)
message = "Value provided for package revision, '{value}' (type {type}) {reason}"
ConanName.validate_name(revision, message=message)
obj = super(cls, PackageReference).__new__(cls, conan, package_id)
obj.revision = revision
return obj
Expand Down
8 changes: 4 additions & 4 deletions conans/test/command/new_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def new_error_test(self):
client = TestClient()
error = client.run('new A/1.3@myuser/testing', ignore_error=True)
self.assertTrue(error)
self.assertIn("ERROR: 'A' is too short. Valid names must contain at least 2 characters.",
client.user_io.out)
self.assertIn("ERROR: Value provided for package name, 'A' (type str), is too short. Valid "
"names must contain at least 2 characters.", client.user_io.out)
error = client.run('new A2/1.3@myuser/u', ignore_error=True)
self.assertTrue(error)
self.assertIn("ERROR: 'u' is too short. Valid names must contain at least 2 characters.",
client.user_io.out)
self.assertIn("ERROR: Value provided for package user, 'u' (type str), is too short. Valid "
"names must contain at least 2 characters.", client.user_io.out)

def new_dash_test(self):
""" packages with dash
Expand Down
54 changes: 53 additions & 1 deletion conans/test/model/ref_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from conans.model.ref import ConanFileReference
from conans.model.ref import ConanFileReference, ConanName, InvalidNameException
from conans.errors import ConanException


Expand Down Expand Up @@ -42,3 +42,55 @@ def errors_test(self):
"opencv/2.4.10@laso%s/testing" % "A" * 40)
self.assertRaises(ConanException, ConanFileReference.loads,
"opencv/2.4.10@laso/testing%s" % "A" * 40)


class ConanNameTestCase(unittest.TestCase):

def _check_invalid_format(self, value, *args):
with self.assertRaisesRegexp(InvalidNameException, "Valid names"):
ConanName.validate_name(value, *args)

def _check_invalid_type(self, value):
with self.assertRaisesRegexp(InvalidNameException, "is not a string"):
ConanName.validate_name(value)

def validate_name_test(self):
self.assertIsNone(ConanName.validate_name("string.dot.under-score.123"))
self.assertIsNone(ConanName.validate_name("_underscore+123"))
self.assertIsNone(ConanName.validate_name("*"))
self.assertIsNone(ConanName.validate_name("a" * ConanName._min_chars))
self.assertIsNone(ConanName.validate_name("a" * ConanName._max_chars))

def validate_name_test_invalid_format(self):
self._check_invalid_format("-no.dash.start")
self._check_invalid_format("a" * (ConanName._min_chars - 1))
self._check_invalid_format("a" * (ConanName._max_chars + 1))

def validate_name_test_invalid_type(self):
self._check_invalid_type(123.34)
self._check_invalid_type(("item1", "item2",))

def validate_name_version_test(self):
self.assertIsNone(ConanName.validate_name("[vvvv]", version=True))

def validate_name_version_test_invalid(self):
self._check_invalid_format("[no.close.bracket", True)
self._check_invalid_format("no.open.bracket]", True)

def custom_message_test_invalid_type(self):
message = "value={value}, type={type}, reason={reason}"
value = 1234
typename = type(value).__name__
reason = "is not a string"
with self.assertRaisesRegexp(InvalidNameException,
message.format(value=value, type=typename, reason=reason)):
ConanName.validate_name(value, message=message)

def custom_message_test_invalid_format(self):
message = "value={value}, type={type}, reason={reason}"
value = "-no.dash.start"
typename = type(value).__name__
reason = "is an invalid name" # Message is longer, but it will validate.
with self.assertRaisesRegexp(InvalidNameException,
message.format(value=value, type=typename, reason=reason)):
ConanName.validate_name(value, message=message)
2 changes: 1 addition & 1 deletion conans/util/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

def get_bool_from_text_value(value):
""" to be deprecated
It has issues, as accepting into the registry whatever=value, as False, withoug
It has issues, as accepting into the registry whatever=value, as False, without
complaining
"""
return (value == "1" or value.lower() == "yes" or value.lower() == "y" or
Expand Down