mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
added -g option to sphinx-build and cleaned out numpy code.
This commit is contained in:
parent
094f201eff
commit
4dbecf132d
@ -95,7 +95,6 @@ def main(argv=sys.argv):
|
||||
for opt, val in opts:
|
||||
|
||||
if opt == '-g':
|
||||
print 'in here'
|
||||
source_filenames =[srcdir+'/'+f for f in os.listdir(srcdir) if f.endswith('.rst')]
|
||||
if val is None:
|
||||
print >>sys.stderr, \
|
||||
|
@ -59,7 +59,6 @@ from docutils import nodes
|
||||
import sphinx.addnodes, sphinx.roles, sphinx.builder
|
||||
from sphinx.util import patfilter
|
||||
|
||||
from docscrape_sphinx import get_doc_object
|
||||
import inspect
|
||||
|
||||
def setup(app):
|
||||
@ -215,30 +214,11 @@ def get_autosummary(names, state, no_signatures=False):
|
||||
|
||||
real_names[name] = real_name
|
||||
|
||||
doc = get_doc_object(obj)
|
||||
|
||||
if doc['Summary']:
|
||||
title = " ".join(doc['Summary'])
|
||||
else:
|
||||
title = ""
|
||||
qualifier = 'obj'
|
||||
if inspect.ismodule(obj):
|
||||
qualifier = 'mod'
|
||||
col1 = ":"+qualifier+":`%s <%s>`" % (name, real_name)
|
||||
if doc['Signature']:
|
||||
sig = re.sub('^[a-zA-Z_0-9.-]*', '',
|
||||
doc['Signature'].replace('*', r'\*'))
|
||||
if '=' in sig:
|
||||
# abbreviate optional arguments
|
||||
sig = re.sub(r', ([a-zA-Z0-9_]+)=', r'[, \1=', sig, count=1)
|
||||
sig = re.sub(r'\(([a-zA-Z0-9_]+)=', r'([\1=', sig, count=1)
|
||||
sig = re.sub(r'=[^,)]+,', ',', sig)
|
||||
sig = re.sub(r'=[^,)]+\)$', '])', sig)
|
||||
# shorten long strings
|
||||
sig = re.sub(r'(\[.{16,16}[^,)]*?),.*?\]\)', r'\1, ...])', sig)
|
||||
else:
|
||||
sig = re.sub(r'(\(.{16,16}[^,)]*?),.*?\)', r'\1, ...)', sig)
|
||||
col1 += " " + sig
|
||||
|
||||
col2 = title
|
||||
append_row(col1, col2)
|
||||
|
||||
|
@ -1,500 +0,0 @@
|
||||
"""Extract reference documentation from the NumPy source tree.
|
||||
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import textwrap
|
||||
import re
|
||||
import pydoc
|
||||
from StringIO import StringIO
|
||||
from warnings import warn
|
||||
|
||||
class Reader(object):
|
||||
"""A line-based string reader.
|
||||
|
||||
"""
|
||||
def __init__(self, data):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
data : str
|
||||
String with lines separated by '\n'.
|
||||
|
||||
"""
|
||||
if isinstance(data,list):
|
||||
self._str = data
|
||||
else:
|
||||
self._str = data.split('\n') # store string as list of lines
|
||||
|
||||
self.reset()
|
||||
|
||||
def __getitem__(self, n):
|
||||
return self._str[n]
|
||||
|
||||
def reset(self):
|
||||
self._l = 0 # current line nr
|
||||
|
||||
def read(self):
|
||||
if not self.eof():
|
||||
out = self[self._l]
|
||||
self._l += 1
|
||||
return out
|
||||
else:
|
||||
return ''
|
||||
|
||||
def seek_next_non_empty_line(self):
|
||||
for l in self[self._l:]:
|
||||
if l.strip():
|
||||
break
|
||||
else:
|
||||
self._l += 1
|
||||
|
||||
def eof(self):
|
||||
return self._l >= len(self._str)
|
||||
|
||||
def read_to_condition(self, condition_func):
|
||||
start = self._l
|
||||
for line in self[start:]:
|
||||
if condition_func(line):
|
||||
return self[start:self._l]
|
||||
self._l += 1
|
||||
if self.eof():
|
||||
return self[start:self._l+1]
|
||||
return []
|
||||
|
||||
def read_to_next_empty_line(self):
|
||||
self.seek_next_non_empty_line()
|
||||
def is_empty(line):
|
||||
return not line.strip()
|
||||
return self.read_to_condition(is_empty)
|
||||
|
||||
def read_to_next_unindented_line(self):
|
||||
def is_unindented(line):
|
||||
return (line.strip() and (len(line.lstrip()) == len(line)))
|
||||
return self.read_to_condition(is_unindented)
|
||||
|
||||
def peek(self,n=0):
|
||||
if self._l + n < len(self._str):
|
||||
return self[self._l + n]
|
||||
else:
|
||||
return ''
|
||||
|
||||
def is_empty(self):
|
||||
return not ''.join(self._str).strip()
|
||||
|
||||
|
||||
class NumpyDocString(object):
|
||||
def __init__(self,docstring):
|
||||
docstring = docstring.split('\n')
|
||||
|
||||
# De-indent paragraph
|
||||
try:
|
||||
indent = min(len(s) - len(s.lstrip()) for s in docstring
|
||||
if s.strip())
|
||||
except ValueError:
|
||||
indent = 0
|
||||
|
||||
for n,line in enumerate(docstring):
|
||||
docstring[n] = docstring[n][indent:]
|
||||
|
||||
self._doc = Reader(docstring)
|
||||
self._parsed_data = {
|
||||
'Signature': '',
|
||||
'Summary': '',
|
||||
'Extended Summary': [],
|
||||
'Parameters': [],
|
||||
'Returns': [],
|
||||
'Raises': [],
|
||||
'Warns': [],
|
||||
'Other Parameters': [],
|
||||
'Attributes': [],
|
||||
'Methods': [],
|
||||
'See Also': [],
|
||||
'Notes': [],
|
||||
'Warnings': [],
|
||||
'References': '',
|
||||
'Examples': '',
|
||||
'index': {}
|
||||
}
|
||||
|
||||
self._parse()
|
||||
|
||||
def __getitem__(self,key):
|
||||
return self._parsed_data[key]
|
||||
|
||||
def __setitem__(self,key,val):
|
||||
if not self._parsed_data.has_key(key):
|
||||
warn("Unknown section %s" % key)
|
||||
else:
|
||||
self._parsed_data[key] = val
|
||||
|
||||
def _is_at_section(self):
|
||||
self._doc.seek_next_non_empty_line()
|
||||
|
||||
if self._doc.eof():
|
||||
return False
|
||||
|
||||
l1 = self._doc.peek().strip() # e.g. Parameters
|
||||
|
||||
if l1.startswith('.. index::'):
|
||||
return True
|
||||
|
||||
l2 = self._doc.peek(1).strip() # ---------- or ==========
|
||||
return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
|
||||
|
||||
def _strip(self,doc):
|
||||
i = 0
|
||||
j = 0
|
||||
for i,line in enumerate(doc):
|
||||
if line.strip(): break
|
||||
|
||||
for j,line in enumerate(doc[::-1]):
|
||||
if line.strip(): break
|
||||
|
||||
return doc[i:len(doc)-j]
|
||||
|
||||
def _read_to_next_section(self):
|
||||
section = self._doc.read_to_next_empty_line()
|
||||
|
||||
while not self._is_at_section() and not self._doc.eof():
|
||||
if not self._doc.peek(-1).strip(): # previous line was empty
|
||||
section += ['']
|
||||
|
||||
section += self._doc.read_to_next_empty_line()
|
||||
|
||||
return section
|
||||
|
||||
def _read_sections(self):
|
||||
while not self._doc.eof():
|
||||
data = self._read_to_next_section()
|
||||
name = data[0].strip()
|
||||
|
||||
if name.startswith('..'): # index section
|
||||
yield name, data[1:]
|
||||
elif len(data) < 2:
|
||||
yield StopIteration
|
||||
else:
|
||||
yield name, self._strip(data[2:])
|
||||
|
||||
def _parse_param_list(self,content):
|
||||
r = Reader(content)
|
||||
params = []
|
||||
while not r.eof():
|
||||
header = r.read().strip()
|
||||
if ' : ' in header:
|
||||
arg_name, arg_type = header.split(' : ')[:2]
|
||||
else:
|
||||
arg_name, arg_type = header, ''
|
||||
|
||||
desc = r.read_to_next_unindented_line()
|
||||
for n,line in enumerate(desc):
|
||||
desc[n] = line.strip()
|
||||
desc = desc #'\n'.join(desc)
|
||||
|
||||
params.append((arg_name,arg_type,desc))
|
||||
|
||||
return params
|
||||
|
||||
|
||||
_name_rgx = re.compile(r"^\s*(:(?P<role>\w+):`(?P<name>[a-zA-Z0-9_.-]+)`|"
|
||||
r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)
|
||||
def _parse_see_also(self, content):
|
||||
"""
|
||||
func_name : Descriptive text
|
||||
continued text
|
||||
another_func_name : Descriptive text
|
||||
func_name1, func_name2, :meth:`func_name`, func_name3
|
||||
|
||||
"""
|
||||
items = []
|
||||
|
||||
def parse_item_name(text):
|
||||
"""Match ':role:`name`' or 'name'"""
|
||||
m = self._name_rgx.match(text)
|
||||
if m:
|
||||
g = m.groups()
|
||||
if g[1] is None:
|
||||
return g[3], None
|
||||
else:
|
||||
return g[2], g[1]
|
||||
raise ValueError("%s is not a item name" % text)
|
||||
|
||||
def push_item(name, rest):
|
||||
if not name:
|
||||
return
|
||||
name, role = parse_item_name(name)
|
||||
items.append((name, list(rest), role))
|
||||
del rest[:]
|
||||
|
||||
current_func = None
|
||||
rest = []
|
||||
|
||||
for line in content:
|
||||
if not line.strip(): continue
|
||||
|
||||
m = self._name_rgx.match(line)
|
||||
if m and line[m.end():].strip().startswith(':'):
|
||||
push_item(current_func, rest)
|
||||
current_func, line = line[:m.end()], line[m.end():]
|
||||
rest = [line.split(':', 1)[1].strip()]
|
||||
if not rest[0]:
|
||||
rest = []
|
||||
elif not line.startswith(' '):
|
||||
push_item(current_func, rest)
|
||||
current_func = None
|
||||
if ',' in line:
|
||||
for func in line.split(','):
|
||||
push_item(func, [])
|
||||
elif line.strip():
|
||||
current_func = line
|
||||
elif current_func is not None:
|
||||
rest.append(line.strip())
|
||||
push_item(current_func, rest)
|
||||
return items
|
||||
|
||||
def _parse_index(self, section, content):
|
||||
"""
|
||||
.. index: default
|
||||
:refguide: something, else, and more
|
||||
|
||||
"""
|
||||
def strip_each_in(lst):
|
||||
return [s.strip() for s in lst]
|
||||
|
||||
out = {}
|
||||
section = section.split('::')
|
||||
if len(section) > 1:
|
||||
out['default'] = strip_each_in(section[1].split(','))[0]
|
||||
for line in content:
|
||||
line = line.split(':')
|
||||
if len(line) > 2:
|
||||
out[line[1]] = strip_each_in(line[2].split(','))
|
||||
return out
|
||||
|
||||
def _parse_summary(self):
|
||||
"""Grab signature (if given) and summary"""
|
||||
if self._is_at_section():
|
||||
return
|
||||
|
||||
summary = self._doc.read_to_next_empty_line()
|
||||
summary_str = " ".join([s.strip() for s in summary]).strip()
|
||||
if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str):
|
||||
self['Signature'] = summary_str
|
||||
if not self._is_at_section():
|
||||
self['Summary'] = self._doc.read_to_next_empty_line()
|
||||
else:
|
||||
self['Summary'] = summary
|
||||
|
||||
if not self._is_at_section():
|
||||
self['Extended Summary'] = self._read_to_next_section()
|
||||
|
||||
def _parse(self):
|
||||
self._doc.reset()
|
||||
self._parse_summary()
|
||||
|
||||
for (section,content) in self._read_sections():
|
||||
if not section.startswith('..'):
|
||||
section = ' '.join([s.capitalize() for s in section.split(' ')])
|
||||
if section in ('Parameters', 'Attributes', 'Methods',
|
||||
'Returns', 'Raises', 'Warns'):
|
||||
self[section] = self._parse_param_list(content)
|
||||
elif section.startswith('.. index::'):
|
||||
self['index'] = self._parse_index(section, content)
|
||||
elif section == 'See Also':
|
||||
self['See Also'] = self._parse_see_also(content)
|
||||
else:
|
||||
self[section] = content
|
||||
|
||||
# string conversion routines
|
||||
|
||||
def _str_header(self, name, symbol='-'):
|
||||
return [name, len(name)*symbol]
|
||||
|
||||
def _str_indent(self, doc, indent=4):
|
||||
out = []
|
||||
for line in doc:
|
||||
out += [' '*indent + line]
|
||||
return out
|
||||
|
||||
def _str_signature(self):
|
||||
if self['Signature']:
|
||||
return [self['Signature'].replace('*','\*')] + ['']
|
||||
else:
|
||||
return ['']
|
||||
|
||||
def _str_summary(self):
|
||||
if self['Summary']:
|
||||
return self['Summary'] + ['']
|
||||
else:
|
||||
return []
|
||||
|
||||
def _str_extended_summary(self):
|
||||
if self['Extended Summary']:
|
||||
return self['Extended Summary'] + ['']
|
||||
else:
|
||||
return []
|
||||
|
||||
def _str_param_list(self, name):
|
||||
out = []
|
||||
if self[name]:
|
||||
out += self._str_header(name)
|
||||
for param,param_type,desc in self[name]:
|
||||
out += ['%s : %s' % (param, param_type)]
|
||||
out += self._str_indent(desc)
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_section(self, name):
|
||||
out = []
|
||||
if self[name]:
|
||||
out += self._str_header(name)
|
||||
out += self[name]
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_see_also(self, func_role):
|
||||
if not self['See Also']: return []
|
||||
out = []
|
||||
out += self._str_header("See Also")
|
||||
last_had_desc = True
|
||||
for func, desc, role in self['See Also']:
|
||||
if role:
|
||||
link = ':%s:`%s`' % (role, func)
|
||||
elif func_role:
|
||||
link = ':%s:`%s`' % (func_role, func)
|
||||
else:
|
||||
link = "`%s`_" % func
|
||||
if desc or last_had_desc:
|
||||
out += ['']
|
||||
out += [link]
|
||||
else:
|
||||
out[-1] += ", %s" % link
|
||||
if desc:
|
||||
out += self._str_indent([' '.join(desc)])
|
||||
last_had_desc = True
|
||||
else:
|
||||
last_had_desc = False
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_index(self):
|
||||
idx = self['index']
|
||||
out = []
|
||||
out += ['.. index:: %s' % idx.get('default','')]
|
||||
for section, references in idx.iteritems():
|
||||
if section == 'default':
|
||||
continue
|
||||
out += [' :%s: %s' % (section, ', '.join(references))]
|
||||
return out
|
||||
|
||||
def __str__(self, func_role=''):
|
||||
out = []
|
||||
out += self._str_signature()
|
||||
out += self._str_summary()
|
||||
out += self._str_extended_summary()
|
||||
for param_list in ('Parameters','Returns','Raises'):
|
||||
out += self._str_param_list(param_list)
|
||||
out += self._str_section('Warnings')
|
||||
out += self._str_see_also(func_role)
|
||||
for s in ('Notes','References','Examples'):
|
||||
out += self._str_section(s)
|
||||
out += self._str_index()
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
def indent(str,indent=4):
|
||||
indent_str = ' '*indent
|
||||
if str is None:
|
||||
return indent_str
|
||||
lines = str.split('\n')
|
||||
return '\n'.join(indent_str + l for l in lines)
|
||||
|
||||
def header(text, style='-'):
|
||||
return text + '\n' + style*len(text) + '\n'
|
||||
|
||||
|
||||
class FunctionDoc(NumpyDocString):
|
||||
def __init__(self, func, role='func'):
|
||||
self._f = func
|
||||
self._role = role # e.g. "func" or "meth"
|
||||
try:
|
||||
NumpyDocString.__init__(self,inspect.getdoc(func) or '')
|
||||
except ValueError, e:
|
||||
print '*'*78
|
||||
print "ERROR: '%s' while parsing `%s`" % (e, self._f)
|
||||
print '*'*78
|
||||
#print "Docstring follows:"
|
||||
#print doclines
|
||||
#print '='*78
|
||||
|
||||
if not self['Signature']:
|
||||
func, func_name = self.get_func()
|
||||
try:
|
||||
# try to read signature
|
||||
argspec = inspect.getargspec(func)
|
||||
argspec = inspect.formatargspec(*argspec)
|
||||
argspec = argspec.replace('*','\*')
|
||||
signature = '%s%s' % (func_name, argspec)
|
||||
except TypeError, e:
|
||||
signature = '%s()' % func_name
|
||||
self['Signature'] = signature
|
||||
|
||||
def get_func(self):
|
||||
func_name = getattr(self._f, '__name__', self.__class__.__name__)
|
||||
if inspect.isclass(self._f):
|
||||
func = getattr(self._f, '__call__', self._f.__init__)
|
||||
else:
|
||||
func = self._f
|
||||
return func, func_name
|
||||
|
||||
def __str__(self):
|
||||
out = ''
|
||||
|
||||
func, func_name = self.get_func()
|
||||
signature = self['Signature'].replace('*', '\*')
|
||||
|
||||
roles = {'func': 'function',
|
||||
'meth': 'method'}
|
||||
|
||||
if self._role:
|
||||
if not roles.has_key(self._role):
|
||||
print "Warning: invalid role %s" % self._role
|
||||
out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''),
|
||||
func_name)
|
||||
|
||||
out += super(FunctionDoc, self).__str__(func_role=self._role)
|
||||
return out
|
||||
|
||||
|
||||
class ClassDoc(NumpyDocString):
|
||||
def __init__(self,cls,modulename='',func_doc=FunctionDoc):
|
||||
if not inspect.isclass(cls):
|
||||
raise ValueError("Initialise using a class. Got %r" % cls)
|
||||
self._cls = cls
|
||||
|
||||
if modulename and not modulename.endswith('.'):
|
||||
modulename += '.'
|
||||
self._mod = modulename
|
||||
self._name = cls.__name__
|
||||
self._func_doc = func_doc
|
||||
|
||||
NumpyDocString.__init__(self, pydoc.getdoc(cls))
|
||||
|
||||
@property
|
||||
def methods(self):
|
||||
return [name for name,func in inspect.getmembers(self._cls)
|
||||
if not name.startswith('_') and callable(func)]
|
||||
|
||||
def __str__(self):
|
||||
out = ''
|
||||
out += super(ClassDoc, self).__str__()
|
||||
out += "\n\n"
|
||||
|
||||
#for m in self.methods:
|
||||
# print "Parsing `%s`" % m
|
||||
# out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n'
|
||||
# out += '.. index::\n single: %s; %s\n\n' % (self._name, m)
|
||||
|
||||
return out
|
||||
|
||||
|
@ -1,133 +0,0 @@
|
||||
import re, inspect, textwrap, pydoc
|
||||
from docscrape import NumpyDocString, FunctionDoc, ClassDoc
|
||||
|
||||
class SphinxDocString(NumpyDocString):
|
||||
# string conversion routines
|
||||
def _str_header(self, name, symbol='`'):
|
||||
return ['.. rubric:: ' + name, '']
|
||||
|
||||
def _str_field_list(self, name):
|
||||
return [':' + name + ':']
|
||||
|
||||
def _str_indent(self, doc, indent=4):
|
||||
out = []
|
||||
for line in doc:
|
||||
out += [' '*indent + line]
|
||||
return out
|
||||
|
||||
def _str_signature(self):
|
||||
return ['']
|
||||
if self['Signature']:
|
||||
return ['``%s``' % self['Signature']] + ['']
|
||||
else:
|
||||
return ['']
|
||||
|
||||
def _str_summary(self):
|
||||
return self['Summary'] + ['']
|
||||
|
||||
def _str_extended_summary(self):
|
||||
return self['Extended Summary'] + ['']
|
||||
|
||||
def _str_param_list(self, name):
|
||||
out = []
|
||||
if self[name]:
|
||||
out += self._str_field_list(name)
|
||||
out += ['']
|
||||
for param,param_type,desc in self[name]:
|
||||
out += self._str_indent(['**%s** : %s' % (param.strip(),
|
||||
param_type)])
|
||||
out += ['']
|
||||
out += self._str_indent(desc,8)
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_section(self, name):
|
||||
out = []
|
||||
if self[name]:
|
||||
out += self._str_header(name)
|
||||
out += ['']
|
||||
content = textwrap.dedent("\n".join(self[name])).split("\n")
|
||||
out += content
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_see_also(self, func_role):
|
||||
out = []
|
||||
if self['See Also']:
|
||||
see_also = super(SphinxDocString, self)._str_see_also(func_role)
|
||||
out = ['.. seealso::', '']
|
||||
out += self._str_indent(see_also[2:])
|
||||
return out
|
||||
|
||||
def _str_warnings(self):
|
||||
out = []
|
||||
if self['Warnings']:
|
||||
out = ['.. warning::', '']
|
||||
out += self._str_indent(self['Warnings'])
|
||||
return out
|
||||
|
||||
def _str_index(self):
|
||||
idx = self['index']
|
||||
out = []
|
||||
if len(idx) == 0:
|
||||
return out
|
||||
|
||||
out += ['.. index:: %s' % idx.get('default','')]
|
||||
for section, references in idx.iteritems():
|
||||
if section == 'default':
|
||||
continue
|
||||
elif section == 'refguide':
|
||||
out += [' single: %s' % (', '.join(references))]
|
||||
else:
|
||||
out += [' %s: %s' % (section, ','.join(references))]
|
||||
return out
|
||||
|
||||
def _str_references(self):
|
||||
out = []
|
||||
if self['References']:
|
||||
out += self._str_header('References')
|
||||
if isinstance(self['References'], str):
|
||||
self['References'] = [self['References']]
|
||||
out.extend(self['References'])
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def __str__(self, indent=0, func_role="obj"):
|
||||
out = []
|
||||
out += self._str_signature()
|
||||
out += self._str_index() + ['']
|
||||
out += self._str_summary()
|
||||
out += self._str_extended_summary()
|
||||
for param_list in ('Parameters', 'Attributes', 'Methods',
|
||||
'Returns','Raises'):
|
||||
out += self._str_param_list(param_list)
|
||||
out += self._str_warnings()
|
||||
out += self._str_see_also(func_role)
|
||||
out += self._str_section('Notes')
|
||||
out += self._str_references()
|
||||
out += self._str_section('Examples')
|
||||
out = self._str_indent(out,indent)
|
||||
return '\n'.join(out)
|
||||
|
||||
class SphinxFunctionDoc(SphinxDocString, FunctionDoc):
|
||||
pass
|
||||
|
||||
class SphinxClassDoc(SphinxDocString, ClassDoc):
|
||||
pass
|
||||
|
||||
def get_doc_object(obj, what=None):
|
||||
if what is None:
|
||||
if inspect.isclass(obj):
|
||||
what = 'class'
|
||||
elif inspect.ismodule(obj):
|
||||
what = 'module'
|
||||
elif callable(obj):
|
||||
what = 'function'
|
||||
else:
|
||||
what = 'object'
|
||||
if what == 'class':
|
||||
return SphinxClassDoc(obj, '', func_doc=SphinxFunctionDoc)
|
||||
elif what in ('function', 'method'):
|
||||
return SphinxFunctionDoc(obj, '')
|
||||
else:
|
||||
return SphinxDocString(pydoc.getdoc(obj))
|
Loading…
Reference in New Issue
Block a user