ソースを参照

Property-ify User; docstring cleanup

tags/v0.1^2
Ben Kurtovic 12年前
コミット
60da4423e2
11個のファイルの変更177行の追加153行の削除
  1. +1
    -1
      earwigbot/commands/afc_report.py
  2. +2
    -2
      earwigbot/commands/editcount.py
  3. +4
    -5
      earwigbot/commands/registration.py
  4. +1
    -1
      earwigbot/commands/rights.py
  5. +1
    -1
      earwigbot/tasks/__init__.py
  6. +1
    -1
      earwigbot/tasks/afc_statistics.py
  7. +3
    -3
      earwigbot/wiki/category.py
  8. +9
    -9
      earwigbot/wiki/page.py
  9. +4
    -4
      earwigbot/wiki/site.py
  10. +52
    -44
      earwigbot/wiki/sitesdb.py
  11. +99
    -82
      earwigbot/wiki/user.py

+ 1
- 1
earwigbot/commands/afc_report.py ファイルの表示

@@ -78,7 +78,7 @@ class Command(BaseCommand):
short = self.statistics.get_short_title(page.title)
status = self.get_status(page)
user = self.site.get_user(page.creator())
user_name = user.name()
user_name = user.name
user_url = user.get_talkpage().url

msg1 = "AfC submission report for \x0302{0}\x0301 ({1}):"


+ 2
- 2
earwigbot/commands/editcount.py ファイルの表示

@@ -45,13 +45,13 @@ class Command(BaseCommand):
user = site.get_user(name)

try:
count = user.editcount()
count = user.editcount
except wiki.UserNotFoundError:
msg = "the user \x0302{0}\x0301 does not exist."
self.reply(data, msg.format(name))
return

safe = quote_plus(user.name())
safe = quote_plus(user.name)
url = "http://toolserver.org/~tparis/pcount/index.php?name={0}&lang=en&wiki=wikipedia"
msg = "\x0302{0}\x0301 has {1} edits ({2})."
self.reply(data, msg.format(name, count, url.format(safe)))

+ 4
- 5
earwigbot/commands/registration.py ファイルの表示

@@ -45,7 +45,7 @@ class Command(BaseCommand):
user = site.get_user(name)

try:
reg = user.registration()
reg = user.registration
except wiki.UserNotFoundError:
msg = "the user \x0302{0}\x0301 does not exist."
self.reply(data, msg.format(name))
@@ -54,14 +54,13 @@ class Command(BaseCommand):
date = time.strftime("%b %d, %Y at %H:%M:%S UTC", reg)
age = self.get_diff(time.mktime(reg), time.mktime(time.gmtime()))

g = user.gender()
if g == "male":
if user.gender == "male":
gender = "He's"
elif g == "female":
elif user.gender == "female":
gender = "She's"
else:
gender = "They're"
msg = "\x0302{0}\x0301 registered on {1}. {2} {3} old."
self.reply(data, msg.format(name, date, gender, age))



+ 1
- 1
earwigbot/commands/rights.py ファイルの表示

@@ -43,7 +43,7 @@ class Command(BaseCommand):
user = site.get_user(name)

try:
rights = user.groups()
rights = user.groups
except wiki.UserNotFoundError:
msg = "the user \x0302{0}\x0301 does not exist."
self.reply(data, msg.format(name))


+ 1
- 1
earwigbot/tasks/__init__.py ファイルの表示

@@ -116,7 +116,7 @@ class BaseTask(object):
except KeyError:
return False
title = cfg.get("page", "User:$1/Shutoff/Task $2")
username = site.get_user().name()
username = site.get_user().name
title = title.replace("$1", username).replace("$2", str(self.number))
page = site.get_page(title)



+ 1
- 1
earwigbot/tasks/afc_statistics.py ファイルの表示

@@ -718,7 +718,7 @@ class Task(BaseTask):
if chart in [self.CHART_PEND, self.CHART_DRAFT] and s_user:
submitter = self.site.get_user(s_user)
try:
if submitter.blockinfo():
if submitter.blockinfo:
notes += "|nb=1" # Submitter is blocked
except wiki.UserNotFoundError: # Likely an IP
pass


+ 3
- 3
earwigbot/wiki/category.py ファイルの表示

@@ -43,12 +43,12 @@ class Category(Page):
"""

def __repr__(self):
"""Returns the canonical string representation of the Category."""
"""Return the canonical string representation of the Category."""
res = "Category(title={0!r}, follow_redirects={1!r}, site={2!r})"
return res.format(self._title, self._follow_redirects, self._site)

def __str__(self):
"""Returns a nice string representation of the Category."""
"""Return a nice string representation of the Category."""
return '<Category "{0}" of {1}>'.format(self.title, str(self._site))

def _get_members_via_sql(self, limit):
@@ -87,7 +87,7 @@ class Category(Page):
return [member["title"] for member in members]

def get_members(self, use_sql=False, limit=None):
"""Returns a list of page titles in the category.
"""Return a list of page titles in the category.

If *use_sql* is ``True``, we will use a SQL query instead of the API.
Pages will be returned as tuples of ``(title, pageid)`` instead of just


+ 9
- 9
earwigbot/wiki/page.py ファイルの表示

@@ -123,12 +123,12 @@ class Page(CopyrightMixin):
self._is_talkpage = self._namespace % 2 == 1

def __repr__(self):
"""Returns the canonical string representation of the Page."""
"""Return the canonical string representation of the Page."""
res = "Page(title={0!r}, follow_redirects={1!r}, site={2!r})"
return res.format(self._title, self._follow_redirects, self._site)

def __str__(self):
"""Returns a nice string representation of the Page."""
"""Return a nice string representation of the Page."""
return '<Page "{0}" of {1}>'.format(self.title, str(self._site))

def _assert_validity(self):
@@ -157,7 +157,7 @@ class Page(CopyrightMixin):
raise exceptions.PageNotFoundError(e)

def _load(self):
"""Calls _load_attributes() and follows redirects if we're supposed to.
"""Call _load_attributes() and follows redirects if we're supposed to.

This method will only follow redirects if follow_redirects=True was
passed to __init__() (perhaps indirectly passed by site.get_page()).
@@ -180,7 +180,7 @@ class Page(CopyrightMixin):
self._load_attributes()

def _load_attributes(self, result=None):
"""Loads various data from the API in a single query.
"""Load various data from the API in a single query.

Loads self._title, ._exists, ._is_redirect, ._pageid, ._fullurl,
._protection, ._namespace, ._is_talkpage, ._creator, ._lastrevid,
@@ -245,7 +245,7 @@ class Page(CopyrightMixin):
pass

def _load_content(self, result=None):
"""Loads current page content from the API.
"""Load current page content from the API.

If *result* is provided, we'll pretend that is the result of an API
query and try to get content from that. Otherwise, we'll do an API
@@ -557,7 +557,7 @@ class Page(CopyrightMixin):
self._load_content()

def toggle_talk(self, follow_redirects=None):
"""Returns a content page's talk page, or vice versa.
"""Return a content page's talk page, or vice versa.

The title of the new page is determined by namespace logic, not API
queries. We won't make any API queries on our own.
@@ -601,7 +601,7 @@ class Page(CopyrightMixin):
return Page(self._site, new_title, follow_redirects)

def get(self):
"""Returns page content, which is cached if you try to call get again.
"""Return page content, which is cached if you try to call get again.

Raises InvalidPageError or PageNotFoundError if the page name is
invalid or the page does not exist, respectively.
@@ -675,7 +675,7 @@ class Page(CopyrightMixin):
return self._site.get_user(self._creator)

def edit(self, text, summary, minor=False, bot=True, force=False):
"""Replaces the page's content or creates a new page.
"""Replace the page's content or creates a new page.

*text* is the new page content, with *summary* as the edit summary.
If *minor* is ``True``, the edit will be marked as minor. If *bot* is
@@ -690,7 +690,7 @@ class Page(CopyrightMixin):
force=force)

def add_section(self, text, title, minor=False, bot=True, force=False):
"""Adds a new section to the bottom of the page.
"""Add a new section to the bottom of the page.

The arguments for this are the same as those for :py:meth:`edit`, but
instead of providing a summary, you provide a section title.


+ 4
- 4
earwigbot/wiki/site.py ファイルの表示

@@ -160,7 +160,7 @@ class Site(object):
self._login(login)

def __repr__(self):
"""Returns the canonical string representation of the Site."""
"""Return the canonical string representation of the Site."""
res = ", ".join((
"Site(name={_name!r}", "project={_project!r}", "lang={_lang!r}",
"base_url={_base_url!r}", "article_path={_article_path!r}",
@@ -179,7 +179,7 @@ class Site(object):
return res.format(login, cookies, agent, **self.__dict__)

def __str__(self):
"""Returns a nice string representation of the Site."""
"""Return a nice string representation of the Site."""
res = "<Site {0} ({1}:{2}) at {3}>"
return res.format(self.name, self.project, self.lang, self.domain)

@@ -704,7 +704,7 @@ class Site(object):
return Page(self, title, follow_redirects)

def get_category(self, catname, follow_redirects=False):
"""Returns a :py:class:`Category` object for the given category name.
"""Return a :py:class:`Category` object for the given category name.

*catname* should be given *without* a namespace prefix. This method is
really just shorthand for :py:meth:`get_page("Category:" + catname)
@@ -715,7 +715,7 @@ class Site(object):
return Category(self, pagename, follow_redirects)

def get_user(self, username=None):
"""Returns a :py:class:`User` object for the given username.
"""Return a :py:class:`User` object for the given username.

If *username* is left as ``None``, then a
:py:class:`~earwigbot.wiki.user.User` object representing the currently


+ 52
- 44
earwigbot/wiki/sitesdb.py ファイルの表示

@@ -35,20 +35,23 @@ __all__ = ["SitesDB"]

class SitesDB(object):
"""
EarwigBot's Wiki Toolset: Sites Database Manager
**EarwigBot's Wiki Toolset: Sites Database Manager**

This class controls the sites.db file, which stores information about all
wiki sites known to the bot. Three public methods act as bridges between
the bot's config files and Site objects:
get_site -- returns a Site object corresponding to a given site name
add_site -- stores a site in the database, given connection info
remove_site -- removes a site from the database, given its name
This class controls the :file:`sites.db` file, which stores information
about all wiki sites known to the bot. Three public methods act as bridges
between the bot's config files and :py:class:`~earwigbot.wiki.site.Site`
objects:

- :py:meth:`get_site`: returns a Site object corresponding to a site
- :py:meth:`add_site`: stores a site in the database
- :py:meth:`remove_site`: removes a site from the database

There's usually no need to use this class directly. All public methods
here are available as bot.wiki.get_site(), bot.wiki.add_site(), and
bot.wiki.remove_site(), which use a sites.db file located in the same
directory as our config.yml file. Lower-level access can be achieved
by importing the manager class (`from earwigbot.wiki import SitesDB`).
here are available as :py:meth:`bot.wiki.get_site`,
:py:meth:`bot.wiki.add_site`, and :py:meth:`bot.wiki.remove_site`, which
use a :file:`sites.db` file located in the same directory as our
:file:`config.yml` file. Lower-level access can be achieved by importing
the manager class (``from earwigbot.wiki import SitesDB``).
"""

def __init__(self, bot):
@@ -157,7 +160,7 @@ class SitesDB(object):
namespaces)

def _make_site_object(self, name):
"""Return a Site object associated with the site 'name' in our sitesdb.
"""Return a Site object associated with the site *name* in our sitesdb.

This calls _load_site_from_sitesdb(), so SiteNotFoundError will be
raised if the site is not in our sitesdb.
@@ -255,24 +258,25 @@ class SitesDB(object):
"""Return a Site instance based on information from the sitesdb.

With no arguments, return the default site as specified by our config
file. This is config.wiki["defaultSite"].
file. This is ``config.wiki["defaultSite"]``.

With 'name' specified, return the site with that name. This is
equivalent to the site's 'wikiid' in the API, like 'enwiki'.
With *name* specified, return the site with that name. This is
equivalent to the site's ``wikiid`` in the API, like *enwiki*.

With 'project' and 'lang' specified, return the site whose project and
With *project* and *lang* specified, return the site whose project and
language match these values. If there are multiple sites with the same
values (unlikely), this is not a reliable way of loading a site. Call
the function with an explicit 'name' in that case.
the function with an explicit *name* in that case.

We will attempt to login to the site automatically using
config.wiki["username"] and config.wiki["password"] if both are
``config.wiki["username"]`` and ``config.wiki["password"]`` if both are
defined.

Specifying a project without a lang or a lang without a project will
raise TypeError. If all three args are specified, 'name' will be first
tried, then 'project' and 'lang' if 'name' doesn't work. If a site
cannot be found in the sitesdb, SiteNotFoundError will be raised. An
raise :py:exc:`TypeError`. If all three args are specified, *name* will
be first tried, then *project* and *lang* if *name* doesn't work. If a
site cannot be found in the sitesdb,
:py:exc:`~earwigbot.exceptions.SiteNotFoundError` will be raised. An
empty sitesdb will be created if none is found.
"""
# Someone specified a project without a lang, or vice versa:
@@ -311,23 +315,27 @@ class SitesDB(object):
script_path="/w", sql=None):
"""Add a site to the sitesdb so it can be retrieved with get_site().

If only a project and a lang are given, we'll guess the base_url as
"//{lang}.{project}.org" (which is protocol-relative, becoming 'https'
if 'useHTTPS' is True in config otherwise 'http'). If this is wrong,
provide the correct base_url as an argument (in which case project and
lang are ignored). Most wikis use "/w" as the script path (meaning the
API is located at "{base_url}{script_path}/api.php" ->
"//{lang}.{project}.org/w/api.php"), so this is the default. If your
wiki is different, provide the script_path as an argument. The only
other argument to Site() that we can't get from config files or by
querying the wiki itself is SQL connection info, so provide a dict of
kwargs as `sql` and Site will pass it to oursql.connect(**sql),
allowing you to make queries with site.sql_query().

Returns True if the site was added successfully or False if the site is
already in our sitesdb (this can be done purposefully to update old
site info). Raises SiteNotFoundError if not enough information has
been provided to identify the site (e.g. a project but not a lang).
If only a project and a lang are given, we'll guess the *base_url* as
``"//{lang}.{project}.org"`` (which is protocol-relative, becoming
``"https"`` if *useHTTPS* is ``True`` in config otherwise ``"http"``).
If this is wrong, provide the correct *base_url* as an argument (in
which case project and lang are ignored). Most wikis use ``"/w"`` as
the script path (meaning the API is located at
``"{base_url}{script_path}/api.php"`` ->
``"//{lang}.{project}.org/w/api.php"``), so this is the default. If
your wiki is different, provide the script_path as an argument. The
only other argument to :py:class:`~earwigbot.wiki.site.Site` that we
can't get from config files or by querying the wiki itself is SQL
connection info, so provide a dict of kwargs as *sql* and Site will
pass it to :py:func:`oursql.connect(**sql) <oursql.connect>`, allowing
you to make queries with :py:meth:`site.sql_query
<earwigbot.wiki.site.Site.sql_query>`.

Returns ``True`` if the site was added successfully or ``False`` if the
site is already in our sitesdb (this can be done purposefully to update
old site info). Raises :py:exc:`~earwigbot.exception.SiteNotFoundError`
if not enough information has been provided to identify the site (e.g.
a *project* but not a *lang*).
"""
if not base_url:
if not project or not lang:
@@ -359,12 +367,12 @@ class SitesDB(object):
def remove_site(self, name=None, project=None, lang=None):
"""Remove a site from the sitesdb.

Returns True if the site was removed successfully or False if the site
was not in our sitesdb originally. If all three args (name, project,
and lang) are given, we'll first try 'name' and then try the latter two
if 'name' wasn't found in the database. Raises TypeError if a project
was given but not a language, or vice versa. Will create an empty
sitesdb if none was found.
Returns ``True`` if the site was removed successfully or ``False`` if
the site was not in our sitesdb originally. If all three args (*name*,
*project*, and *lang*) are given, we'll first try *name* and then try
the latter two if *name* wasn't found in the database. Raises
:py:exc:`TypeError` if a project was given but not a language, or vice
versa. Will create an empty sitesdb if none was found.
"""
# Someone specified a project without a lang, or vice versa:
if (project and not lang) or (not project and lang):


+ 99
- 82
earwigbot/wiki/user.py ファイルの表示

@@ -30,28 +30,33 @@ __all__ = ["User"]

class User(object):
"""
EarwigBot's Wiki Toolset: User Class
**EarwigBot's Wiki Toolset: User Class**

Represents a User on a given Site. Has methods for getting a bunch of
information about the user, such as editcount and user rights, methods for
returning the user's userpage and talkpage, etc.
Represents a user on a given :py:class:`~earwigbot.wiki.site.Site`. Has
methods for getting a bunch of information about the user, such as
editcount and user rights, methods for returning the user's userpage and
talkpage, etc.

Attributes:
name -- the user's username
exists -- True if the user exists, or False if they do not
userid -- an integer ID representing the user
blockinfo -- information about any current blocks on the user
groups -- a list of the user's groups
rights -- a list of the user's rights
editcount -- the number of edits made by the user
registration -- the time the user registered as a time.struct_time
emailable -- True if you can email the user, False if you cannot
gender -- the user's gender ("male", "female", or "unknown")

- :py:attr:`name`: the user's username
- :py:attr:`exists`: ``True`` if the user exists, else ``False``
- :py:attr:`userid`: an integer ID representing the user
- :py:attr:`blockinfo`: information about any current blocks on the user
- :py:attr:`groups`: a list of the user's groups
- :py:attr:`rights`: a list of the user's rights
- :py:attr:`editcount`: the number of edits made by the user
- :py:attr:`registration`: the time the user registered
- :py:attr:`emailable`: ``True`` if you can email the user, or ``False``
- :py:attr:`gender`: the user's gender ("male"/"female"/"unknown")

Public methods:
reload -- forcibly reload the user's attributes
get_userpage -- returns a Page object representing the user's userpage
get_talkpage -- returns a Page object representing the user's talkpage

- :py:meth:`reload`: forcibly reloads the user's attributes
- :py:meth:`get_userpage`: returns a Page object representing the user's
userpage
- :py:meth:`get_talkpage`: returns a Page object representing the user's
talkpage
"""

def __init__(self, site, name):
@@ -71,26 +76,25 @@ class User(object):
self._name = name

def __repr__(self):
"""Returns the canonical string representation of the User."""
"""Return the canonical string representation of the User."""
return "User(name={0!r}, site={1!r})".format(self._name, self._site)

def __str__(self):
"""Returns a nice string representation of the User."""
return '<User "{0}" of {1}>'.format(self.name(), str(self._site))
"""Return a nice string representation of the User."""
return '<User "{0}" of {1}>'.format(self._name, str(self._site))

def _get_attribute(self, attr, force):
def _get_attribute(self, attr):
"""Internally used to get an attribute by name.

We'll call _load_attributes() to get this (and all other attributes)
from the API if it is not already defined. If `force` is True, we'll
re-load them even if they've already been loaded.
from the API if it is not already defined.

Raises UserNotFoundError if a nonexistant user prevents us from
returning a certain attribute.
"""
if not hasattr(self, attr) or force:
if not hasattr(self, attr):
self._load_attributes()
if self._exists is False:
if not self._exists:
e = "User '{0}' does not exist.".format(self._name)
raise UserNotFoundError(e)
return getattr(self, attr)
@@ -150,105 +154,118 @@ class User(object):

self._gender = res["gender"]

def name(self, force=False):
"""Returns the user's name.

If `force` is True, we will load the name from the API and return that.
This could potentially return a "normalized" version of the name - for
example, without a "User:" prefix or without underscores. Unlike other
attribute getters, this will never make an API query without `force`.
@property
def name(self):
"""The user's username.

Note that if another attribute getter, like exists(), has already been
called, then the username has already been normalized.
This will never make an API query on its own, but if one has already
been made by the time this is retrieved, the username may have been
"normalized" from the original input to the constructor, converted into
a Unicode object, with underscores removed, etc.
"""
if force:
self._load_attributes()
return self._name

def exists(self, force=False):
"""Returns True if the user exists, or False if they do not.
@property
def exists(self):
"""``True`` if the user exists, or ``False`` if they do not.

Makes an API query if `force` is True or if we haven't made one
already.
Makes an API query only if we haven't made one already.
"""
if not hasattr(self, "_exists") or force:
if not hasattr(self, "_exists"):
self._load_attributes()
return self._exists

def userid(self, force=False):
"""Returns an integer ID used by MediaWiki to represent the user.
@property
def userid(self):
"""An integer ID used by MediaWiki to represent the user.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_userid", force)
return self._get_attribute("_userid")

def blockinfo(self, force=False):
"""Returns information about a current block on the user.
@property
def blockinfo(self):
"""Information about any current blocks on the user.

If the user is not blocked, returns False. If they are, returns a dict
with three keys: "by" is the blocker's username, "reason" is the reason
why they were blocked, and "expiry" is when the block expires.
If the user is not blocked, returns ``False``. If they are, returns a
dict with three keys: ``"by"`` is the blocker's username, ``"reason"``
is the reason why they were blocked, and ``"expiry"`` is when the block
expires.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_blockinfo", force)

def groups(self, force=False):
"""Returns a list of groups this user is in, including "*".
@property
def groups(self):
"""A list of groups this user is in, including ``"*"``.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_groups", force)

def rights(self, force=False):
"""Returns a list of this user's rights.
@property
def rights(self):
"""A list of this user's rights.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_rights", force)

def editcount(self, force=False):
@property
def editcount(self):
"""Returns the number of edits made by the user.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_editcount", force)

def registration(self, force=False):
"""Returns the time the user registered as a time.struct_time object.
@property
def registration(self):
"""The time the user registered as a :py:class:`time.struct_time`.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_registration", force)

def emailable(self, force=False):
"""Returns True if the user can be emailed, or False if they cannot.
@property
def emailable(self):
"""``True`` if the user can be emailed, or ``False`` if they cannot.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_emailable", force)

def gender(self, force=False):
"""Returns the user's gender.
@property
def gender(self):
"""The user's gender.

Can return either "male", "female", or "unknown", if they did not
specify it.
Can return either ``"male"``, ``"female"``, or ``"unknown"``, if they
did not specify it.

Raises UserNotFoundError if the user does not exist. Makes an API query
if `force` is True or if we haven't made one already.
Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
does not exist. Makes an API query only if we haven't made one already.
"""
return self._get_attribute("_gender", force)

def reload(self):
"""Forcibly reload the user's attributes.

Emphasis on *reload*: this is only necessary if there is reason to
believe they have changed.
"""
self._load_attributes()

def get_userpage(self):
"""Returns a Page object representing the user's userpage.
"""Return a Page object representing the user's userpage.
No checks are made to see if it exists or not. Proper site namespace
conventions are followed.
"""
@@ -257,8 +274,8 @@ class User(object):
return Page(self._site, pagename)

def get_talkpage(self):
"""Returns a Page object representing the user's talkpage.
"""Return a Page object representing the user's talkpage.
No checks are made to see if it exists or not. Proper site namespace
conventions are followed.
"""


読み込み中…
キャンセル
保存