Skip to content
This repository

Validation tutorial #128

Merged
merged 7 commits into from about 2 years ago

3 participants

Alex Railean Burak Arslan Jan Wąsak
Alex Railean

Some basic examples, nothing major - but they should help one get started.

Burak Arslan
Owner
plq commented March 19, 2012

hi alex,

first, thanks a lot for your efforts. here are my remarks:

1) you're talking about the advanced validation methods without talking about the simpler ones. E.g. Integer(ge=1, le=12) will check for integers between 1 and 12 inclusive. or String(pattern='some_regex') will validate the incoming string using regexps. actually, the validation can be done using lxml's schema validator if you use these methods. if you override validation functions, only validator='soft' can check them.

2) validating by overriding the validation functions don't change the schema itself. so using them are not really encouraged.

3) I always use the "rhetorical we" in the documentation. We should rather be consistent about that one.

4) I always wrap documentation and code by 80 columns. You can see that it's too hard to read even using the web interface.

Again, thanks for your time.

Alex Railean

Hi, thanks for the comments.

I was aware of the regexp pattern for the validation of strings, but I didn't know about the approach you used with Integers. I'll add these examples.

Some questions:

  1. are there other tricks besides the regexp pattern matching for strings and the comparison operators for numbers?
  2. are there any performance considerations one should be aware of? Is the underlying lxml method validation method faster? If so, is it simply because it is "underneath" and there are no abstraction layers above it? Is it because lxml wraps C code? Anything else?
  3. are there any good reasons to use the "soft" validation methods instead of the simple ones? I think the example with prime numbers is fine, but I'm not sure about the other one. I chose to write it like that because "if ':' in value" is a simple check that doesn't involve regular expressions (which are most likely going to be slower). Is that the case?
  4. validation by overriding the functions doesn't change the schema itself, so people who use this SOAP service won't be aware of the actual constraints that are applied to the data. Are there other arguments against "soft" validation?

Style remarks:

  1. wrap at 80 lines
  2. no "I"; this reminds me of http://en.wikipedia.org/wiki/A_Time_of_Changes :-)
  3. is there anything else? I think I will create another section in the manual about this; so others can be synchronized

Questions that are related to other aspects I want to document:

  1. How to declare attributes? Can you provide a simple definition of a class that declares attributes such as "Instant", "MajorVersion" and "MinorVersion" in this schema http://pastebin.com/3hgStRi9 ? Preferably, the example should include optional and mandatory attributes
  2. The same schema has 'abstract="true" ' in the first line, I'm not sure how to express that in terms of rpclib either
  3. Please provide an example of creating an Enum. I saw enum.py in /model and I suppose that you use it like this: myEnum = Enum( ("foo", "bar"), type_name="MyCustomEnum"). Is there anything else one should keep in mind? Will this work: myEnum = Enum( (1, 3, 5), type_name="MyOtherEnum") ?
  4. Line 15 of the same schema contains this: type="mss:MeshMemberType". If I interpret this correctly, it refers to another namespace. To implement that, must I override the namespace variable in the class of the custom type with "mss:MeshMemberType"?

Thanks forward and good night :-)

edited to add numbers to make it easier to respond to individual points. --b.

Burak Arslan
Owner
plq commented March 19, 2012

Part I

  1. Yes, you have values constraint for every type. Integer(values=[1,2,3]) or Unicode(values=["a","b"]) or DateTime(values=[datetime.now()]) Also string types have a max_len constraint. You can check the class named Attributes that is defined inside every ModelBase child for possible declarative constraints.

  2. I'd imagine lxml's validation would be much faster because it's a call to C code. I didn't do any benchmarks though.

  3. Soft validation is done by python code. lxml validation won't tolerate one wrong byte, whereas soft validation is more forgiving, especially regarding namespaces. of course, lxml validation can only work on xml data. the prime numbers validation will be skipped when lxml validation is used, because it's just python code.

  4. None that I can think of. Soft validation could use a little bit more testing though. imho, lxml validation is rock solid.

Part II

  1. Columns

  2. Heh, thanks for the reference :) It seems that UKL further developed that idea in Disposessed.

  3. Code conforms with pep8. I can't think of anything else now.

Part III

  1. http://mail.python.org/pipermail/soap/2012-February/000736.html

  2. no abc support in rpclib yet.

  3. https://github.com/arskom/rpclib/blob/master/src/rpclib/test/model/test_enum.py

class MeshMemberType(ComplexModel):
    __namespace__ = "some_string"

Many examples can be found inside tests. They're also a valuable part of the documentation.

Alex Railean - xml schema constraints
- notes about the schema not being updated in the case of advanced validation wizardry
7423988
Alex Railean

I applied the changes, here are some additional questions:

  1. For custom types derived from other types, rpclib will call the validation functions of each type in the inheritance chain. True or False?
  2. XML schema validation doesn't work for floats, is it true? I didn't see an Attribute class in the Float type declaration and I didn't see any code that would compare floats properly (i.e. within a margin of error)
  3. Creating new XML schema level constraints by adding an Attributes class - can this be done?

Thanks for the other examples, I will tinker with that. So far I haven't yet began implementing my own system with rpclib, but I think there will be more questions as I begin working on it.

Burak Arslan

u"alpha", u"bravo", u"charlie"

no actual enforcement of this, but that's the right way to do it.

Burak Arslan
Owner
plq commented March 20, 2012

this is going great so far. a few comments:

  1. lxml validation and soft validation don't co-operate. you either use one or the other. They should behave mostly the same though. The expected differences are:

    • Soft validation ignores unknown fields, whereas lxml validation just rejects them.
    • Soft validation doesn't care about namespaces, whereas lxml validation rejects unexpected namespaces.
    • Errors thrown by soft and lxml validation differ quite a lot (even client-visible ones). Maybe this is a bug.

    So, if you're going to do imperative validation, you should use soft validation. If your validation rules can be expressed by the declarative tools rpclib offers you, you should use lxml validation, which is, as I said earlier, much more mature than rpclib's built-in validation.

    I guess the above could be added to the relevant section :)

    I think we should call these "validation subsystems" rather than "validation layers" because the term "layer" makes it sound like they actually cooperate. they do not, not currently at least.

  2. This document is a little bit too XML-centric. I understand that currently this is an area where rpclib is more popular, but non-xml protocols can't be validated using lxml's schema validation -- only soft validation will work there. I think this should be made explicit and any xml-related discussion should happen in its own section.

  3. I also made small remarks in-line in the commit page.

Burak Arslan
Owner
plq commented March 20, 2012

As for your questions:

  1. False. The overriding function must call its bases' validation. That may not be always necessery or desired.

  2. No proper float comparison either, no. I wonder how lxml deals with this when validating floats though. It'd be a nice question to ask to the libxml people, I guess.

  3. Yes, but you should modify the schema generation code as well, new fields in Attributes class are not picked up automatically

Alex Railean - xml validation only in soap/xml
- typo fix
- adding custom Attribute
bb26041
Alex Railean

I applied the changes, here are some additional questions about this doc:

  1. Are non-unicode strings converted to unicode at the lower levels? What's the side effect of "charlie"?
  2. You can use one XOR the other, hmmmm... So, you mean that if I used lxml to check if the thing is a number, later I cannot use the 'soft' validator to see if it is prime?
  3. Do the users see some kind of a warning that says "you're using both, disable one"?
  4. If the code attempts to use both methods, which one is actually applied? (assuming that the code works and no exceptions are thrown at the time when the classes are loaded)
  5. "Errors thrown by soft and lxml validation differ quite a lot" - can you provide an example? Is this worthy of mentioning in the doc? Is the delta in readability, or in something else? Somehow I imagine errors thrown by lxml are similar to errors shown by C++ compilers that complain about templates :-)
  6. Floats... Hmm, maybe I will experiment with this some time later and reflect this in the doc. The problem is that I haven't yet written my system and haven't even set up an environment that enables me to run rpclib with a debugger. So, if you can tell me anything else about it - do so, otherwise I'll get back to this matter later, when I learn more about lxml's behaviour. In any case, comparing floats without taking another argument such as "error margin" doesn't sound right. Maybe lxml uses a default value for that? We'll see.

There are some additional questions about the other things you said, but they're not related to validation. I think it is better if I take that to the soap mailing list; otherwise those questions are off-topic in this context.

Alex Railean

7 DateTime(values=[datetime.now()]) - how do I interpret that? The only allowed value for this variable is the current system time? Does this ever happen in practice?

Burak Arslan
Owner
plq commented March 20, 2012
  1. No, no conversion is done. There's no side effect if you stay in ascii. Be prepared for a random UnicodeError if it's not.

  2. Correct. You need to use the soft validation to do non-standard validation.

  3. No. That's impossible to detect 100% due to python's dynamic nature.

  4. You can't use both because you set validator='soft' or validator='lxml' when instantiating the protocol.

  5. Not off the top of my head. They are both human readable, so unless you're trying to parse them, you're fine. And yes, lxml's errors are more, um, "verbose", though not as bad as g++'s templated class errors.

  6. I'd just leave that be for the time being.

  7. Yes this means what you think it means. i don't think this makes sense at all either, it was just an example :)

Burak Arslan

it should be set to 1. making the type mandatory is not as trivial though. For example for string, you need to have String(min_occurs=1, min_len=1, nillable=False) to make it mandatory. see https://github.com/arskom/rpclib/blob/4f57b4abf284e51baca904d6649c8747ebc2804a/src/rpclib/model/primitive.py#L601

Burak Arslan

This will change for rpclib-2.8.0. You should set it to float('inf'), the native infinite value, to have arbitrary number of args.

Alex Railean - ``unbounded`` in v2.8.0
- comparison table, soft vs lxml
- min_occurs 0->1
2dc0f05
Burak Arslan
Owner
plq commented March 21, 2012

Hi Alex,

I have no further comments. Unless there is anything else I can help you with, I'm going to merge this.

Thanks!

Alex Railean

No, just one remark: http://arskom.github.com/rpclib/reference/model.html#rpclib.model._base.ModelBase

On this page it says
{
min_occurs = 0
Set this to 0 to make the type mandatory. Can be set to any positive integer.
}

One of them must be wrong :-)

Burak Arslan plq merged commit 804e68e into from March 21, 2012
Burak Arslan plq closed this March 21, 2012
Burak Arslan plq referenced this pull request from a commit March 21, 2012
Commit has since been removed from the repository and is no longer available.
Burak Arslan
Owner
plq commented March 21, 2012

just fixed that, and the document is merged. thank you for your time.

Burak Arslan plq referenced this pull request from a commit in plq/spyne March 21, 2012
Burak Arslan update min_occurs docstring in ModelBase class. #128 0059bd7
Jan Wąsak

Hi,

I am very happy after reading this, helped me solve some problems I was experiencing with my little project. Only one thing comes to mind at this point - you mention generic restrictions on primitive types with example but there is no example code for ComplexType derivatives. Maybe just a small snippet? I for one, love the 'learn by example' approach of python documentation.

class MyClass(ComplexModel):
    """
    lucky guess at complex type retrictions
    """
    __namespace__ = "partner"
    class Attributes(object):
        min_occurs = 0
        max_occurs = 1
        default = None
        nillable = False
Burak Arslan
Owner
plq commented March 27, 2012

the attributes class should inherit from its parent. here's the correct version:

class MyClass(ComplexModel):
    """lucky guess at complex type retrictions"""

    __namespace__ = "partner"
    __type_name__ = "RealClassName"

    class Attributes(ComplexModel.Attributes):
        min_occurs = 0
        max_occurs = 1
        default = None
        nillable = False

if you think it's going to be helpful, send a small pull request and i'll make sure it gets decent attention :)

Alex Railean

Hi,

I am planning to write more documentation about this, and I had the same thing in mind - just add a bunch of examples at the bottom, with comments embedded into them.

I will do so in the next revision, but at the moment I am focused on documenting another part of rpclib - the creation of custom types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 7 unique commits by 1 author.

Mar 19, 2012
Alex Railean First iteration, not sure everything is entirely correct. 80b4396
Alex Railean Update doc/source/manual/validation.rst 49bdad7
Alex Railean Update doc/source/manual/validation.rst 946ab77
Mar 20, 2012
Alex Railean - xml schema constraints
- notes about the schema not being updated in the case of advanced validation wizardry
7423988
Alex Railean - xml validation only in soap/xml
- typo fix
- adding custom Attribute
bb26041
Mar 21, 2012
Alex Railean Added restrictions that can be applied to any type (minOccurs, maxOcc…
…urs, nillable, default).
131c443
Alex Railean - ``unbounded`` in v2.8.0
- comparison table, soft vs lxml
- min_occurs 0->1
2dc0f05
This page is out of date. Refresh to see the latest.

Showing 1 changed file with 301 additions and 2 deletions. Show diff stats Hide diff stats

  1. 303  doc/source/manual/validation.rst
303  doc/source/manual/validation.rst
Source Rendered
... ...
@@ -1,7 +1,306 @@
1  
-
2 1
 .. _manual-validation:
3 2
 
4 3
 Input Validation
5 4
 ================
  5
+This is necessary in the cases in which you have to ensure that the received 
  6
+data comply with a given format, such as:
  7
+
  8
+- a number must be within a certain range
  9
+- a string that must contain a specific character
  10
+- a string that can only take certain values
  11
+
  12
+
  13
+Data validation can be handled by two subsystems:
  14
+
  15
+XML schema
  16
+	such rules are enforced by *lxml*, the underlying XML parsing library 
  17
+"Soft" level
  18
+	*rpclib* itself can apply additional checks after the data were validated by
  19
+	the layer underneath
  20
+
  21
+The differences between them are:
  22
+
  23
+- Soft validation ignores unknown fields, while *lxml* validation rejects 
  24
+  them.
  25
+- Soft validation doesn't care about namespaces, while *lxml* validation 
  26
+  rejects unexpected namespaces.
  27
+- Soft validation works with any transport protocol supported by *rpclib*,
  28
+  while *lxml* validation only works for XML data (i.e. just SOAP/XML).
  29
+
  30
+============================== ======== =========                            
  31
+Criteria                       lxml     soft
  32
+============================== ======== =========
  33
+Unknown fields                 reject   ignore
  34
+Unknown namespaces             reject   ignore
  35
+Supported transport protocols  SOAP/XML any
  36
+============================== ======== =========
  37
+
  38
+
  39
+	
  40
+
  41
+.. NOTE::
  42
+	The two validation sybsystems operate independently, you can use either one,
  43
+	but not both at the same time. The validator is indicated when instantiating
  44
+	the protocol: ``validator='soft'`` or ``validator='lxml'``.
  45
+	
  46
+	::
  47
+
  48
+		#using 'soft' validation with HttpRpc
  49
+		application = Application([NameOfMonthService],
  50
+			tns='rpclib.examples.multiprot',
  51
+			in_protocol=HttpRpc(validator='soft'),
  52
+			out_protocol=HttpRpc()
  53
+			)
  54
+
  55
+		#using lxml validation with Soap
  56
+		application = Application([UserService],
  57
+			tns='rpclib.examples.authentication',
  58
+			interface=Wsdl11(),
  59
+			in_protocol=Soap11(validator='lxml'),
  60
+			out_protocol=Soap11()
  61
+			)
  62
+
  63
+	
  64
+
  65
+
  66
+Simple validation at the XML schema level
  67
+-----------------------------------------
  68
+This applies to all the primitive data types, and is suitable for simple logical
  69
+conditions.
  70
+
  71
+.. NOTE::
  72
+	Constraints applied at this level are reflected in the XML schema itself,
  73
+	thus a client that retrieves the WSDL of the service will be able to see
  74
+	what the constraints are.
  75
+	As it was mentioned in the introduction, such validation is only effective
  76
+	in the context of SOAP/XML.
  77
+
  78
+
  79
+Any primitive type
  80
+~~~~~~~~~~~~~~~~~~
  81
+Certain generic restrictions can be applied to any type. They are listed below,
  82
+along with their default values
  83
+
  84
+- ``default = None`` - default value if the input is ``None``
  85
+- ``nillable = True`` - if True, the item is optional
  86
+- ``min_occurs = 0`` - set this to 1 to make the type mandatory. Can be set to 
  87
+  any positive integer
  88
+- ``max_occurs = 1`` - can be set to any strictly positive integer. Values 
  89
+  greater than 1 will imply an iterable of objects as native Python type. Can be
  90
+  set to ``unbounded`` for arbitrary number of arguments
  91
+  
  92
+  .. NOTE::
  93
+	As of rpclib-2.8.0, use ``float('inf')`` instead of ``unbounded``.
  94
+  
  95
+These rules can be combined, the example below illustrates how to create a
  96
+mandatory string:
  97
+
  98
+	String(min_occurs=1, min_len=1, nillable=False)
  99
+	
  100
+
  101
+Numbers
  102
+~~~~~~~
  103
+Integers and other countable numerical data types (i.e. except Float or 
  104
+Double) can be compared with specific values, using the following keywords: 
  105
+``ge``, ``gt``, ``le``, ``lt`` (they correspond to >=, >, <=, <) ::
  106
+
  107
+	Integer(ge=1, le=12) #an integer between 1 and 12, i.e. 1 <= x <= 12
  108
+	Integer(gt=1, le=42) #1 < x <= 42
  109
+	
  110
+
  111
+Strings
  112
+~~~~~~~
  113
+These can be validated against a regular expression: ::
  114
+
  115
+	String(pattern = "[0-9]+") #must contain at least one digit, digits only 
  116
+	
  117
+	
  118
+Length checks can be enforced as well: ::
  119
+
  120
+	String(min_len = 5, max_len = 10)
  121
+	String(max_len = 10) #implicit value for min_len = 0
  122
+
  123
+
  124
+Other string-related constraints are related to encoding issues. You can specify
  125
+
  126
+- which encoding the strings must be in
  127
+- how to handle the situations in which a string cannot be decoded properly (to
  128
+  understand how this works, consult `Python's documentation 
  129
+  <http://docs.python.org/howto/unicode.html>`_ ::
  130
+
  131
+        String(encoding = 'win-1251')
  132
+        String(unicode_errors = 'strict') #could be 'replace' or 'ignore'
  133
+
  134
+		
  135
+These restrictions can be combined: ::
  136
+
  137
+	String(encoding = 'win-1251', max_len = 20)
  138
+	String(min_len = 5, max_len = 20, pattern = '[a-z]')
  139
+	
  140
+
  141
+Possible values
  142
+~~~~~~~~~~~~~~~
  143
+Sometimes you may want to allow only a certain set of values, which would be
  144
+difficult to describe in terms of an interval. If this is the case, you can
  145
+explicitly indicate the set: ::
  146
+
  147
+	Integer(values = [1984, 13, 45, 42])
  148
+	Unicode(values = [u"alpha", u"bravo", u"charlie"]) #note the 'u' prefix
  149
+	
  150
+
  151
+
  152
+Extending the rules of XML validation
  153
+-------------------------------------
  154
+It is possible to add your own attributes to the XML schema and enforce them.
  155
+
  156
+
  157
+To do so, create an ``Attributes`` in the definition of your custom type derived
  158
+from ``ModelBase``.
  159
+
  160
+
  161
+After that, you must apply the relevant changes in the code that generates the
  162
+XML schema, otherwise these attributes will **not** be visible in the output. 
  163
+
  164
+Examples of how to do that:
  165
+https://github.com/arskom/rpclib/tree/master/src/rpclib/interface/xml_schema/model
  166
+
  167
+
  168
+
  169
+
  170
+
  171
+Advanced validation
  172
+-------------------
  173
+*rpclib* offers several primitives for this purpose, they are defined in 
  174
+the **ModelBase** class, from which all the types are derived:
  175
+https://github.com/arskom/rpclib/blob/master/src/rpclib/model/_base.py
  176
+
  177
+These primitives are:
  178
+
  179
+- *validate_string* - invoked when the variable is extracted from the input XML
  180
+  data.
  181
+- *validate_native* - invoked after the string is converted to a specific Python
  182
+  value.
  183
+
  184
+Since XML is a text file, when you read it - you get a string; thus 
  185
+*validate_string* is the first filter that can be applied to such data. 
  186
+
  187
+At a later stage, the data can be converted to something else, for example - a
  188
+number. Once that conversion occurs, you can apply some additional checks - this
  189
+is handled by *validate_native*.
  190
+
  191
+	>>> stringNumber = '123'
  192
+	>>> stringNumber
  193
+	'123'		#note the quotes, it is a string
  194
+	>>> number = int(stringNumber)
  195
+	>>> number
  196
+	123 		#notice the absence of quotes, it is a number
  197
+	>>> stringNumber == 123
  198
+	False		#note quite what one would expect, right?
  199
+	>>> number == 123
  200
+	True
  201
+
  202
+In the example above, *number* is an actual number and can be validated with 
  203
+*validate_native*, whereas *stringNumber* is a string and can be validated by 
  204
+*validate_string*.
  205
+
  206
+
  207
+Another case in which you need a native validation would be a sanity check on a 
  208
+date. Imagine that you have to verify if a received date complies with the 
  209
+*"YYYY-MM-DDThh:mm:ss"* pattern (which is *xs:datetime*). You can devise a 
  210
+regular expression that will look for 4 digits (YYYY), followed by a dash, then
  211
+by 2 more digits for the month, etc. But such a regexp will happily absorb dates
  212
+that have "13" as a month number, even though that doesn't make sense. You can
  213
+make a more complex regexp to deal with that, but it will be very hard to 
  214
+maintain and debug. The best approach is to convert the string into a datetime
  215
+object and then perform all the checks you want.
  216
+
  217
+
  218
+
  219
+A practical example
  220
+~~~~~~~~~~~~~~~~~~~
  221
+A custom string type that cannot contain the colon symbol ':'.
  222
+
  223
+We'll have to declare our own class, derived from *Unicode* (which, in turn, is
  224
+derived from *SimpleModel*, which inherits from *ModelBase*).::
  225
+
  226
+
  227
+	class SpecialString(Unicode):
  228
+		"""Custom string type that prohibis the use of colons"""
  229
+		
  230
+		@staticmethod
  231
+		def validate_string(cls, value):
  232
+			"""Override the function to enforce our own verification logic"""
  233
+			if value:
  234
+				if ':' in value:
  235
+					return True
  236
+			return False
  237
+
  238
+
  239
+
  240
+A slightly more complicated example
  241
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  242
+A custom numerical type that verifies if the number is prime.
  243
+
  244
+This time both flavours of validation are combined: *validate_string* to see if
  245
+it is a number, and then *validate_native* to see if it is prime.
  246
+
  247
+.. NOTE::
  248
+	*rpclib* has a primitive type called *Integer*, it is reasonable to use that
  249
+	one as a basis for this custom type. *Unicode* is used in this example
  250
+	simply because it is an opportunity to show both types of validation
  251
+	functions in action. This may be a good academic example, but it is 
  252
+	certainly not the approach one would use in production code.
  253
+
  254
+
  255
+::
  256
+
  257
+	class PrimeNumber(Unicode):
  258
+		"""Custom integer type that only works with prime numbers"""
  259
+		
  260
+		@staticmethod
  261
+		def validate_string(cls, value):
  262
+			"""See if it is a number"""
  263
+			import re
  264
+						
  265
+			if re.search("[0-9]+", value):
  266
+				return True
  267
+			else:
  268
+				return False
  269
+
  270
+		@staticmethod
  271
+		def validate_native(cls, value):
  272
+			"""See if it is prime"""
  273
+			
  274
+			#calling a hypothetical function that checks if it is prime
  275
+			return IsPrime(value)
  276
+
  277
+
  278
+.. NOTE::
  279
+	Constraints applied at this level do **not modify** the XML schema itself,
  280
+	thus a client that retrieves the WSDL of the service will not be aware of
  281
+	these restrictions. Keep this in mind and make sure that validation rules
  282
+	that are not visible in the XML schema are documented elsewhere.
  283
+			
  284
+.. NOTE::
  285
+	When overriding ``validate_string`` or ``validate_native`` in a custom type
  286
+	class, the validation functions from the parent class are **not invoked**.
  287
+	If you wish to apply those validation functions as well, you must call them
  288
+	explicitly.
  289
+
  290
+
  291
+		
  292
+Summary
  293
+=======
  294
+- simple checks can be applied at the XML schema level, you can control:
  295
+
  296
+  - the length of a string
  297
+  - the pattern with which a string must comply
  298
+  - a numeric interval, etc
  299
+  
  300
+- *rpclib* can apply arbitrary rules for the validation of input data
6 301
 
7  
-TODO
  302
+  - *validate_string* is the first applied filter
  303
+  - *validate_native* is the applied at the second phase
  304
+  - Override these functions in your derived class to add new validation rules
  305
+  - The validation functions must return a *boolean* value
  306
+  - These rules are **not** shown in the XML schema
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.